Repository: smithy-lang/smithy
Pull request #1905 (2023-08-02 22:07:59)

Support enum map keys on OpenAPI 3.1.0
Closes: https://github.com/smithy-lang/smithy/issues/1664

This PR updates OpenAPI configuration to allow `propertyNames` to be set when using version `3.1.0`.

Previously, `propertyNames` was stripped from outputs as an unsupported keyword, since OpenAPI 3.0 did not support it.

By allowing `propertyNames`, enums can be used as map keys, without discarding all of the enum information.

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/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d70c06398..4ae8f88b8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -28,6 +28,10 @@ jobs:
       - name: Clean, build and javadoc
         run: ./gradlew clean build javadoc -Plog-tests --stacktrace
 
+      - name: Allow long file names in git for windows
+        if: matrix.os == 'windows-latest'
+        run: git config --system core.longpaths true
+
       - name: CLI integration tests
         if: matrix.java >= 17
         run: ./gradlew :smithy-cli:integ -Plog-tests --stacktrace
diff --git a/.github/workflows/update-brew.yml b/.github/workflows/update-brew.yml
index 97c2e19f9..23e1e3839 100644
--- a/.github/workflows/update-brew.yml
+++ b/.github/workflows/update-brew.yml
@@ -62,7 +62,7 @@ jobs:
         with:
           repository: 'smithy-lang/homebrew-tap'
           path: 'homebrew-tap'
-          ref: 'master'
+          ref: 'main'
       - name: Update version
         run: |
           version="${GITHUB_REF##*/}"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1981809c1..677f55290 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,34 @@
 # Smithy Changelog
 
+## 1.36.0 (2023-08-03)
+
+### Features
+* Allowed disabling format on integers when converting to OpenAPI ([#1904](https://github.com/smithy-lang/smithy/pull/1904))
+* Added intEnum support when converting to OpenAPI ([#1898](https://github.com/smithy-lang/smithy/pull/1898))
+* Added support for overriding validation severity ([#1890](https://github.com/smithy-lang/smithy/pull/1890))
+* Added `disableDefaultValues` option when converting to OpenAPI ([#1887](https://github.com/smithy-lang/smithy/pull/1887))
+* Updated brew workflow to use new smithy tap ([#1897](https://github.com/smithy-lang/smithy/pull/1897))
+* Added progress tracker and message for CLI while cloning a template ([#1888](https://github.com/smithy-lang/smithy/pull/1888))
+* Updated init command to honor quiet setting ([#1889](https://github.com/smithy-lang/smithy/pull/1889))
+* Updated appearance of smithy init list output ([#1901](https://github.com/smithy-lang/smithy/pull/1901))
+* Added exceptions for invalid paths in template definition ([#1907](https://github.com/smithy-lang/smithy/pull/1907))
+* Added Cache template directory in init command ([#1896](https://github.com/smithy-lang/smithy/pull/1896))
+* Check for existing directory when creating template with init ([#1885](https://github.com/smithy-lang/smithy/pull/1885))
+
+### Documentation
+* Clarified constraint trait enforcement ([#1902](https://github.com/smithy-lang/smithy/pull/1902))
+* Better document trait merging ([#1895](https://github.com/smithy-lang/smithy/pull/1895))
+* Updated docs to use new smithy-lang tap ([#1893](https://github.com/smithy-lang/smithy/pull/1893))
+* Added naming recommendations ([#1892](https://github.com/smithy-lang/smithy/pull/1892))
+
+### Bug Fixes
+* Fixed NPE when docId is null when ServiceTrait.equals is called ([#1903](https://github.com/smithy-lang/smithy/pull/1903))
+* Fixed off-by-one issues in TokenTree and TreeCursor ([#1891](https://github.com/smithy-lang/smithy/pull/1891))
+* Fixed snapshot dependency resolution ([#1884](https://github.com/smithy-lang/smithy/pull/1884))
+
+### Other
+* Use standard output for regular messages ([#1894](https://github.com/smithy-lang/smithy/pull/1894))
+
 ## 1.35.0 (2023-07-27)
 
 ### Features
diff --git a/VERSION b/VERSION
index 2aeaa11ee..39fc130ef 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.35.0
+1.36.0
diff --git a/docs/source-2.0/spec/model.rst b/docs/source-2.0/spec/model.rst
index 3430edb0b..fc96282ca 100644
--- a/docs/source-2.0/spec/model.rst
+++ b/docs/source-2.0/spec/model.rst
@@ -1250,67 +1250,18 @@ Prelude
 
 All Smithy models automatically include a *prelude*. The prelude defines
 various simple shapes and every trait defined in the core specification.
-When using the :ref:`IDL <idl>`, shapes defined in the prelude can be
-referenced from within any namespace using a relative shape ID.
+When using the :ref:`IDL <idl>`, shapes defined in the prelude that are not
+marked with the :ref:`private-trait` can be referenced from within any
+namespace using a relative shape ID.
 
-.. code-block:: smithy
-    :caption: Public prelude shapes
+.. literalinclude:: ../../../smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy
+    :language: smithy
+    :caption: Smithy Prelude
     :name: prelude-shapes
 
-    $version: "2"
-    namespace smithy.api
-
-    string String
-
-    blob Blob
-
-    bigInteger BigInteger
-
-    bigDecimal BigDecimal
-
-    timestamp Timestamp
-
-    document Document
-
-    boolean Boolean
-
-    byte Byte
-
-    short Short
-
-    integer Integer
-
-    long Long
-
-    float Float
-
-    double Double
-
-    /// The single unit type shape, similar to Void and None in other
-    /// languages, used to represent no meaningful value.
-    @unitType
-    structure Unit {}
-
-    @default(false)
-    boolean PrimitiveBoolean
-
-    @default(0)
-    byte PrimitiveByte
-
-    @default(0)
-    short PrimitiveShort
-
-    @default(0)
-    integer PrimitiveInteger
-
-    @default(0)
-    long PrimitiveLong
-
-    @default(0)
-    float PrimitiveFloat
+.. note::
 
-    @default(0)
-    double PrimitiveDouble
+    Private shapes defined in the prelude are subject to change at any time.
 
 
 .. _unit-type:
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/IntegUtils.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/IntegUtils.java
index 22015c527..636a2609d 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/IntegUtils.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/IntegUtils.java
@@ -39,7 +39,9 @@ import software.amazon.smithy.utils.IoUtils;
 import software.amazon.smithy.utils.MapUtils;
 
 public final class IntegUtils {
-
+    public static final Path SMITHY_ROOT_CACHE_PATH = Paths.get(System.getProperty("java.io.tmpdir"))
+            .resolve("smithy-cache");
+    public static final Path SMITHY_TEMPLATE_CACHE_PATH = SMITHY_ROOT_CACHE_PATH.resolve("templates");
     private static final Logger LOGGER = Logger.getLogger(IntegUtils.class.getName());
 
     private IntegUtils() {}
@@ -146,4 +148,8 @@ public final class IntegUtils {
             }
         });
     }
+
+    public static void clearCacheDirIfExists() {
+        IoUtils.rmdir(SMITHY_ROOT_CACHE_PATH);
+    }
 }
diff --git a/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/smithy-templates/smithy-templates.json b/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/smithy-templates/smithy-templates.json
index cde68000b..2f9d6a00b 100644
--- a/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/smithy-templates/smithy-templates.json
+++ b/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/smithy-templates/smithy-templates.json
@@ -18,6 +18,17 @@
                 "getting-started-example/common-gradle-configs/gradlew",
                 "getting-started-example/common-gradle-configs/gradle.properties"
             ]
+        },
+        "bad-template-path": {
+            "documentation": "Template with incorrect path. Should throw error.",
+            "path": "getting-started-example/does-not-exist"
+        },
+        "bad-include-path": {
+            "documentation": "Template with incorrect include path. Should throw error.",
+            "path": "getting-started-example/included-files-gradle",
+            "include": [
+                "getting-started-example/does-not-exist"
+            ]
         }
     }
 }
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/ColorTheme.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/ColorTheme.java
index 5e012d005..e4aa1842c 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/ColorTheme.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/ColorTheme.java
@@ -49,5 +49,8 @@ public final class ColorTheme {
 
     public static final Style HINT_TITLE = Style.of(Style.BRIGHT_GREEN);
 
+    public static final Style TEMPLATE_TITLE = Style.of(Style.BOLD, Style.BRIGHT_WHITE);
+    public static final Style TEMPLATE_LIST_BORDER = Style.of(Style.BRIGHT_WHITE);
+
     private ColorTheme() {}
 }
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CleanCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CleanCommand.java
index 8873302bd..d861e9459 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CleanCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CleanCommand.java
@@ -20,8 +20,10 @@ import java.nio.file.Paths;
 import java.util.logging.Logger;
 import software.amazon.smithy.build.SmithyBuild;
 import software.amazon.smithy.build.model.SmithyBuildConfig;
+import software.amazon.smithy.cli.ArgumentReceiver;
 import software.amazon.smithy.cli.Arguments;
 import software.amazon.smithy.cli.Command;
+import software.amazon.smithy.cli.HelpPrinter;
 import software.amazon.smithy.utils.IoUtils;
 
 final class CleanCommand implements Command {
@@ -40,20 +42,32 @@ final class CleanCommand implements Command {
 
     @Override
     public String getSummary() {
-        return "Removes Smithy build artifacts.";
+        return "Removes Smithy build artifacts and caches.";
     }
 
     @Override
     public int execute(Arguments arguments, Env env) {
         arguments.addReceiver(new ConfigOptions());
+        arguments.addReceiver(new Options());
 
         CommandAction action = HelpActionWrapper.fromCommand(this, parentCommandName, this::run);
         return action.apply(arguments, env);
     }
 
     private int run(Arguments arguments, Env env) {
-        ConfigOptions options = arguments.getReceiver(ConfigOptions.class);
-        SmithyBuildConfig config = options.createSmithyBuildConfig();
+        ConfigOptions configOptions = arguments.getReceiver(ConfigOptions.class);
+        Options options = arguments.getReceiver(Options.class);
+
+        if (options.cleanTemplateCache) {
+            LOGGER.fine(() -> "Clearing template cache.");
+            if (CliCache.getTemplateCache().clear()) {
+                LOGGER.fine(() -> "No template cache found.");
+            }
+            return 0;
+        }
+
+
+        SmithyBuildConfig config = configOptions.createSmithyBuildConfig();
         Path dir = config.getOutputDirectory()
                 .map(Paths::get)
                 .orElseGet(SmithyBuild::getDefaultOutputDirectory);
@@ -62,6 +76,35 @@ final class CleanCommand implements Command {
             LOGGER.fine(() -> "Directory does not exist: " + dir);
         }
         LOGGER.fine(() -> "Deleted directory " + dir);
+
+        LOGGER.fine(() -> "Clearing all caches.");
+        if (!CliCache.clearAll()) {
+            LOGGER.fine(() -> "No caches found.");
+        }
+
         return 0;
     }
+
+
+    private static final class Options implements ArgumentReceiver {
+        private Boolean cleanTemplateCache = false;
+
+        @Override
+        public boolean testOption(String name) {
+            switch (name) {
+                case "--templates":
+                case "-t":
+                    cleanTemplateCache = true;
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public void registerHelp(HelpPrinter printer) {
+            printer.param("--templates", "-t", null,
+                    "Clean only the templates cache.");
+        }
+    }
 }
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CliCache.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CliCache.java
new file mode 100644
index 000000000..7d588d1d2
--- /dev/null
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CliCache.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.cli.commands;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import software.amazon.smithy.cli.CliError;
+import software.amazon.smithy.utils.IoUtils;
+
+interface CliCache {
+    Path DEFAULT_TEMP_DIR = Paths.get(System.getProperty("java.io.tmpdir"));
+    Path ROOT_CACHE_DIR = DEFAULT_TEMP_DIR.resolve("smithy-cache");
+
+    static CliCache getTemplateCache() {
+        return () -> ROOT_CACHE_DIR.resolve("templates");
+    }
+
+    Path getPath();
+
+    default boolean clear() {
+        return IoUtils.rmdir(getPath());
+    }
+
+    default Path get() {
+        Path cachePath = getPath();
+        if (Files.exists(cachePath)) {
+            return cachePath;
+        }
+        // If cache dir does not exist, create it and all required parent directories
+        try {
+            return Files.createDirectories(cachePath);
+        } catch (IOException e) {
+            throw new CliError("Could not create cache at path: " + cachePath);
+        }
+    }
+
+    static boolean clearAll() {
+        return IoUtils.rmdir(ROOT_CACHE_DIR);
+    }
+}
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/InitCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/InitCommand.java
index bd4a85c22..3200734f4 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/InitCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/InitCommand.java
@@ -16,8 +16,8 @@
 package software.amazon.smithy.cli.commands;
 
 import java.io.IOException;
-import java.net.URISyntaxException;
 import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -41,8 +41,12 @@ import software.amazon.smithy.model.node.ObjectNode;
 import software.amazon.smithy.model.node.StringNode;
 import software.amazon.smithy.utils.IoUtils;
 import software.amazon.smithy.utils.ListUtils;
+import software.amazon.smithy.utils.StringUtils;
+
 
 final class InitCommand implements Command {
+    private static final int LINE_LENGTH = 100;
+    private static final String COLUMN_SEPARATOR = "  ";
     private static final String SMITHY_TEMPLATE_JSON = "smithy-templates.json";
     private static final String DEFAULT_REPOSITORY_URL = "https://github.com/smithy-lang/smithy-examples.git";
     private static final String DEFAULT_TEMPLATE_NAME = "quickstart-cli";
@@ -86,79 +90,156 @@ final class InitCommand implements Command {
         Options options = arguments.getReceiver(Options.class);
         StandardOptions standardOptions = arguments.getReceiver(StandardOptions.class);
 
-        try {
-            Path root = Paths.get(".");
-            Path temp = Files.createTempDirectory("temp");
+        boolean isLocalRepo = isLocalRepo(options.repositoryUrl);
+        Path templateRepoDirPath = getTemplateRepoDirPath(options.repositoryUrl, isLocalRepo);
+
+        // If the cache directory does not exist, create it
+        if (!Files.exists(templateRepoDirPath)) {
+            try (ProgressTracker t = new ProgressTracker(env,
+                    ProgressStyle.dots("cloning template repo", "template repo cloned"),
+                    standardOptions.quiet()
+            )) {
+                Path templateCachePath = CliCache.getTemplateCache().get();
+                String relativeTemplateDir = templateCachePath.relativize(templateRepoDirPath).toString();
+                // Only clone the latest commit from HEAD. Do not include history
+                exec(ListUtils.of("git", "clone", "--depth", "1", "--single-branch",
+                        options.repositoryUrl, relativeTemplateDir), templateCachePath);
+            }
+        }
 
-            loadSmithyTemplateJsonFile(options.repositoryUrl, root, temp);
+        validateTemplateDir(templateRepoDirPath, options.repositoryUrl);
+
+        // Check for updates and update repo to latest if applicable
+        if (!isLocalRepo) {
+            // update remote
+            exec(ListUtils.of("git", "fetch", "--depth", "1"), templateRepoDirPath);
+            String response = exec(ListUtils.of("git", "rev-list", "origin..HEAD"), templateRepoDirPath);
+            // If a change was detected, force the template repo to update
+            if (!StringUtils.isEmpty(response)) {
+                try (ProgressTracker t = new ProgressTracker(env,
+                        ProgressStyle.dots("updating template cache", "template repo updated"),
+                        standardOptions.quiet()
+                )) {
+                    exec(ListUtils.of("git", "reset", "--hard", "origin/main"), templateRepoDirPath);
+                    exec(ListUtils.of("git", "clean", "-dfx"), templateRepoDirPath);
+                }
+            }
+        }
 
-            ObjectNode smithyTemplatesNode = getSmithyTemplatesNode(temp);
 
-            if (options.listTemplates) {
-                this.listTemplates(smithyTemplatesNode, env);
-            } else {
-                this.cloneTemplate(temp, smithyTemplatesNode, options, standardOptions, env);
-            }
-        } catch (IOException | InterruptedException | URISyntaxException e) {
-            throw new RuntimeException(e);
+        ObjectNode smithyTemplatesNode = readJsonFileAsNode(templateRepoDirPath.resolve(SMITHY_TEMPLATE_JSON))
+                .expectObjectNode();
+        if (options.listTemplates) {
+            this.listTemplates(smithyTemplatesNode, env);
+        } else {
+            this.cloneTemplate(templateRepoDirPath, smithyTemplatesNode, options, standardOptions, env);
         }
 
         return 0;
     }
 
-    private void listTemplates(ObjectNode smithyTemplatesNode, Env env) throws IOException {
+    private void validateTemplateDir(Path templateRepoDirPath, String templateUrl) {
+        if (!Files.isDirectory(templateRepoDirPath)) {
+            throw new CliError("Template repository " + templateRepoDirPath + " is not a directory");
+        }
+        Path templateJsonPath = templateRepoDirPath.resolve(SMITHY_TEMPLATE_JSON);
+        if (!Files.exists(templateJsonPath) && Files.isRegularFile(templateJsonPath)) {
+            throw new CliError("Template repository " + templateUrl
+                    + " does not contain a valid `smithy-templates.json`.");
+        }
+    }
+
+    private Path getTemplateRepoDirPath(String repoPath, boolean isLocalRepo) {
+        if (isLocalRepo) {
+            // Just use the local path if the git repo is local
+            return Paths.get(repoPath);
+        } else {
+            return CliCache.getTemplateCache().get().resolve(getCacheDirFromURL(repoPath));
+        }
+    }
+
+    // Remove any trailing .git
+    // Remove "/" and ".." and ":" characters so a directory can be created with no nesting
+    private String getCacheDirFromURL(final String repositoryUrl) {
+        return repositoryUrl.replace(".git", "")
+                .replace(":", "_")
+                .replace("/", "_")
+                .replace(".", "_");
+    }
+
+    private void listTemplates(ObjectNode smithyTemplatesNode, Env env) {
         try (ColorBuffer buffer = ColorBuffer.of(env.colors(), env.stdout())) {
             buffer.println(getTemplateList(smithyTemplatesNode, env));
         }
     }
 
+    private boolean isLocalRepo(String repoPath) {
+        try  {
+            Path localPath = Paths.get(repoPath);
+            return Files.exists(localPath);
+        } catch (InvalidPathException exc) {
+            return false;
+        }
+
+    }
+
     private String getTemplateList(ObjectNode smithyTemplatesNode, Env env) {
         int maxTemplateLength = 0;
-        int maxDocumentationLength = 0;
         Map<String, String> templates = new TreeMap<>();
-
         for (Map.Entry<StringNode, Node> entry : getTemplatesNode(smithyTemplatesNode).getMembers().entrySet()) {
             String template = entry.getKey().getValue();
             String documentation = entry.getValue()
-                .expectObjectNode()
-                .expectMember(DOCUMENTATION, String.format(
-                        "Missing expected member `%s` from `%s` object", DOCUMENTATION, template))
-                .expectStringNode()
-                .getValue();
+                    .expectObjectNode()
+                    .expectMember(DOCUMENTATION, String.format(
+                            "Missing expected member `%s` from `%s` object", DOCUMENTATION, template))
+                    .expectStringNode()
+                    .getValue();
 
             templates.put(template, documentation);
 
             maxTemplateLength = Math.max(maxTemplateLength, template.length());
-            maxDocumentationLength = Math.max(maxDocumentationLength, documentation.length());
         }
+        int maxDocLength = LINE_LENGTH - maxTemplateLength - COLUMN_SEPARATOR.length();
 
-        final String space = "   ";
+        ColorBuffer builder = ColorBuffer.of(env.colors(), new StringBuilder());
 
-        ColorBuffer builder = ColorBuffer.of(env.colors(), new StringBuilder())
-                .print(pad(NAME.toUpperCase(Locale.US), maxTemplateLength), ColorTheme.LITERAL)
-                .print(space)
-                .print(DOCUMENTATION.toUpperCase(Locale.US), ColorTheme.LITERAL)
-                .println()
-                .print(pad("", maxTemplateLength).replace(' ', '-'), ColorTheme.MUTED)
-                .print(space)
-                .print(pad("", maxDocumentationLength).replace(' ', '-'), ColorTheme.MUTED)
+        writeTemplateBorder(builder, maxTemplateLength, maxDocLength);
+        builder.print(pad(NAME.toUpperCase(Locale.US), maxTemplateLength), ColorTheme.NOTE)
+                .print(COLUMN_SEPARATOR)
+                .print(DOCUMENTATION.toUpperCase(Locale.US), ColorTheme.NOTE)
                 .println();
+        writeTemplateBorder(builder, maxTemplateLength, maxDocLength);
+
+        int offset = maxTemplateLength + COLUMN_SEPARATOR.length();
 
         for (Map.Entry<String, String> entry : templates.entrySet()) {
             String template = entry.getKey();
             String doc = entry.getValue();
-            builder.print(pad(template, maxTemplateLength))
-                    .print(space)
-                    .print(pad(doc, maxDocumentationLength))
+
+            builder.print(pad(template, maxTemplateLength), ColorTheme.TEMPLATE_TITLE)
+                    .print(COLUMN_SEPARATOR)
+                    .print(wrapDocumentation(doc, maxDocLength, offset),
+                            ColorTheme.MUTED)
                     .println();
         }
 
         return builder.toString();
     }
 
-    private void cloneTemplate(Path temp, ObjectNode smithyTemplatesNode, Options options,
-                               StandardOptions standardOptions, Env env)
-            throws IOException, InterruptedException, URISyntaxException {
+
+    private static void writeTemplateBorder(ColorBuffer writer, int maxNameLength, int maxDocLength) {
+        writer.print(pad("", maxNameLength).replace(" ", "─"), ColorTheme.TEMPLATE_LIST_BORDER)
+                .print(COLUMN_SEPARATOR)
+                .print(pad("", maxDocLength).replace(" ", "─"), ColorTheme.TEMPLATE_LIST_BORDER)
+                .println();
+    }
+
+    private static String wrapDocumentation(String doc, int maxLength, int offset) {
+        return StringUtils.wrap(doc, maxLength, System.lineSeparator() + pad("", offset), false);
+    }
+
+    private void cloneTemplate(Path templateRepoDirPath, ObjectNode smithyTemplatesNode, Options options,
+                               StandardOptions standardOptions, Env env) {
 
         String template = options.template;
         String directory = options.directory;
@@ -171,7 +252,7 @@ final class InitCommand implements Command {
         if (directory == null) {
             directory = template;
         }
-        final Path dest = Paths.get(directory);
+        Path dest = Paths.get(directory);
         if (Files.exists(dest)) {
             throw new CliError("Output directory `" + directory + "` already exists.");
         }
@@ -182,27 +263,27 @@ final class InitCommand implements Command {
                     "Invalid template `%s`. `%s` provides the following templates:%n%n%s",
                     template, getTemplatesName(smithyTemplatesNode), getTemplateList(smithyTemplatesNode, env)));
         }
-
         ObjectNode templateNode = templatesNode.expectObjectMember(template).expectObjectNode();
 
-        final String templatePath = getTemplatePath(templateNode, template);
+        String templatePath = getTemplatePath(templateNode, template);
         List<String> includedFiles = getIncludedFiles(templateNode);
 
-        try (ProgressTracker t = new ProgressTracker(env,
-                ProgressStyle.dots("cloning template", "template cloned"),
-                standardOptions.quiet()
-        )) {
-            // Specify the subdirectory to download
-            exec(ListUtils.of("git", "sparse-checkout", "set", "--no-cone", templatePath), temp);
-            // add any additional files that should be included
-            for (String includedFile : includedFiles) {
-                exec(ListUtils.of("git", "sparse-checkout", "add", "--no-cone", includedFile), temp);
-            }
-            exec(ListUtils.of("git", "checkout"), temp);
+        Path stagingPath = createStagingRepo(templateRepoDirPath);
+
+        // Specify the subdirectory to check out
+        exec(ListUtils.of("git", "sparse-checkout", "set", "--no-cone", templatePath), stagingPath);
+        // add any additional files that should be included
+        for (String includedFile : includedFiles) {
+            exec(ListUtils.of("git", "sparse-checkout", "add", "--no-cone", includedFile), stagingPath);
         }
+        exec(ListUtils.of("git", "checkout"), stagingPath);
 
-        IoUtils.copyDir(Paths.get(temp.toString(), templatePath), dest);
-        copyIncludedFiles(temp.toString(), dest.toString(), includedFiles, template, env);
+        if (!Files.exists(stagingPath.resolve(templatePath))) {
+            throw new CliError(String.format("Template path `%s` for template \"%s\" is invalid.",
+                    templatePath, template));
+        }
+        IoUtils.copyDir(Paths.get(stagingPath.toString(), templatePath), dest);
+        copyIncludedFiles(stagingPath.toString(), dest.toString(), includedFiles, template, env);
 
         if (!standardOptions.quiet()) {
             try (ColorBuffer buffer = ColorBuffer.of(env.colors(), env.stdout())) {
@@ -211,13 +292,17 @@ final class InitCommand implements Command {
         }
     }
 
-    private static void loadSmithyTemplateJsonFile(String repositoryUrl, Path root, Path temp) {
-        exec(ListUtils.of("git", "clone", "--filter=blob:none", "--no-checkout", "--depth", "1", "--sparse",
-                repositoryUrl, temp.toString()), root);
-
-        exec(ListUtils.of("git", "sparse-checkout", "set", "--no-cone", SMITHY_TEMPLATE_JSON), temp);
+    private static Path createStagingRepo(Path repoPath) {
+        Path temp;
+        try {
+            temp = Files.createTempDirectory("temp");
+        } catch (IOException exc) {
+            throw new CliError("Unable to create staging directory for template.");
+        }
+        exec(ListUtils.of("git", "clone", "--no-checkout", "--depth", "1", "--sparse",
+                "file://" + repoPath.toString(), temp.toString()), Paths.get("."));
 
-        exec(ListUtils.of("git", "checkout"), temp);
+        return temp;
     }
 
     private static ObjectNode getSmithyTemplatesNode(Path jsonFilePath) {
@@ -253,22 +338,25 @@ final class InitCommand implements Command {
     }
 
     private static void copyIncludedFiles(String temp, String dest, List<String> includedFiles,
-                                          String templateName, Env env) throws IOException {
+                                          String templateName, Env env) {
         for (String included : includedFiles) {
             Path includedPath = Paths.get(temp, included);
             if (!Files.exists(includedPath)) {
-                try (ColorBuffer buffer = ColorBuffer.of(env.colors(), env.stderr())) {
-                    buffer.println(String.format(
-                            "File or directory %s is marked for inclusion in template %s but was not found",
-                            included, templateName), ColorTheme.WARNING);
-                }
+                throw new CliError(String.format(
+                        "File or directory `%s` is marked for inclusion in template \"%s\", but was not found",
+                        included, templateName));
             }
 
             Path target = Paths.get(dest, Objects.requireNonNull(includedPath.getFileName()).toString());
             if (Files.isDirectory(includedPath)) {
                 IoUtils.copyDir(includedPath, target);
             } else if (Files.isRegularFile(includedPath)) {
-                Files.copy(includedPath, target);
+                try {
+                    Files.copy(includedPath, target);
+                } catch (IOException e) {
+                    throw new CliError("Unable to copy included file: " + includedPath
+                            + "to destination directory: " + target);
+                }
             }
         }
     }
diff --git a/smithy-diff/README.md b/smithy-diff/README.md
index 98c86a7c9..4ad5dd42b 100644
--- a/smithy-diff/README.md
+++ b/smithy-diff/README.md
@@ -14,6 +14,9 @@ Model modelB = loadModelB();
 List<ValidationEvent> events = ModelDiff.compare(modelA, modelB);
 ```
 
+Smithy Diff will load and apply metadata suppressions and severityOverrides
+from the new model to any emitted diff events. 
+
 # Adding a custom DiffEvaluator
 
 This library finds all instances of `DiffEvaluator`
diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java
index 3e18a774d..cb5c6b881 100644
--- a/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java
+++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java
@@ -27,6 +27,8 @@ import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.validation.Severity;
 import software.amazon.smithy.model.validation.ValidatedResult;
 import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidationEventDecorator;
+import software.amazon.smithy.model.validation.suppressions.ModelBasedEventDecorator;
 import software.amazon.smithy.utils.SmithyBuilder;
 
 /**
@@ -280,8 +282,17 @@ public final class ModelDiff {
             List<DiffEvaluator> evaluators = new ArrayList<>();
             ServiceLoader.load(DiffEvaluator.class, classLoader).forEach(evaluators::add);
             Differences differences = Differences.detect(oldModel, newModel);
+
+            // Applies suppressions and elevates event severities.
+            ValidationEventDecorator decoratorResult = new ModelBasedEventDecorator()
+                    .createDecorator(newModel)
+                    .getResult()
+                    .orElse(ValidationEventDecorator.IDENTITY);
+
             List<ValidationEvent> diffEvents = evaluators.parallelStream()
                     .flatMap(evaluator -> evaluator.evaluate(differences).stream())
+                    // No need to call canDecorate first since that method will always return true in any code path.
+                    .map(decoratorResult::decorate)
                     .collect(Collectors.toList());
 
             return new Result(differences, diffEvents, oldModelEvents, newModelEvents);
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java
index af9284f52..85edc2c20 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java
@@ -24,22 +24,18 @@ import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import software.amazon.smithy.model.Model;
-import software.amazon.smithy.model.SourceException;
 import software.amazon.smithy.model.SourceLocation;
-import software.amazon.smithy.model.node.ObjectNode;
-import software.amazon.smithy.model.shapes.Shape;
-import software.amazon.smithy.model.shapes.ShapeId;
-import software.amazon.smithy.model.traits.SuppressTrait;
 import software.amazon.smithy.model.validation.Severity;
 import software.amazon.smithy.model.validation.ValidatedResult;
 import software.amazon.smithy.model.validation.ValidationEvent;
 import software.amazon.smithy.model.validation.ValidationEventDecorator;
 import software.amazon.smithy.model.validation.Validator;
 import software.amazon.smithy.model.validation.ValidatorFactory;
+import software.amazon.smithy.model.validation.suppressions.ModelBasedEventDecorator;
 import software.amazon.smithy.model.validation.suppressions.SeverityOverride;
-import software.amazon.smithy.model.validation.suppressions.Suppression;
 import software.amazon.smithy.model.validation.validators.ResourceCycleValidator;
 import software.amazon.smithy.model.validation.validators.TargetValidator;
+import software.amazon.smithy.utils.ListUtils;
 import software.amazon.smithy.utils.SetUtils;
 import software.amazon.smithy.utils.SmithyBuilder;
 
@@ -65,9 +61,6 @@ import software.amazon.smithy.utils.SmithyBuilder;
  */
 final class ModelValidator implements Validator {
 
-    private static final String SUPPRESSIONS = "suppressions";
-    private static final String SEVERITY_OVERRIDES = "severityOverrides";
-
     // Lazy initialization holder class idiom to hold a default validator factory.
     private static final class LazyValidatorFactoryHolder {
         static final ValidatorFactory INSTANCE = ValidatorFactory.createServiceFactory(
@@ -194,7 +187,6 @@ final class ModelValidator implements Validator {
     private static final class LoadedModelValidator {
 
         private final Model model;
-        private final List<Suppression> suppressions = new ArrayList<>();
         private final List<SeverityOverride> severityOverrides;
         private final List<Validator> validators;
         private final List<ValidationEvent> events = new ArrayList<>();
@@ -203,56 +195,38 @@ final class ModelValidator implements Validator {
 
         private LoadedModelValidator(Model model, ModelValidator validator) {
             this.model = model;
-            this.validationEventDecorator = validator.validationEventDecorator;
             this.eventListener = validator.eventListener;
             this.severityOverrides = new ArrayList<>(validator.severityOverrides);
             this.validators = new ArrayList<>(validator.validators);
 
-            loadMetadataSuppressions();
-            loadMetadataSeverityOverrides();
+            // Suppressing and elevating events is handled by composing a given decorator with a
+            // ModelBasedEventDecorator.
+            ModelBasedEventDecorator modelBasedEventDecorator = new ModelBasedEventDecorator();
+            modelBasedEventDecorator.severityOverrides(validator.severityOverrides);
+            ValidatedResult<ValidationEventDecorator> result = modelBasedEventDecorator.createDecorator(model);
+            this.validationEventDecorator = result.getResult()
+                    .map(decorator -> ValidationEventDecorator.compose(
+                            ListUtils.of(decorator, validator.validationEventDecorator)))
+                    .orElse(validator.validationEventDecorator);
+
+            // Events encountered while loading suppressions and overrides have been modified by everything the
+            // modelBasedEventDecorator knows about, but has not been modified by any custom decorator (if any).
+            for (ValidationEvent event : result.getValidationEvents()) {
+                if (validationEventDecorator.canDecorate(event)) {
+                    event = validationEventDecorator.decorate(event);
+                }
+                events.add(event);
+            }
 
-            // Given events have already been emitted and decorated, but have not been suppressed/elevated.
+            // Now that the decorator is available, emit/decorate/suppress/collect explicitly provided events.
             for (ValidationEvent event : validator.events) {
-                events.add(modifyEventSeverity(event));
+                pushEvent(event);
             }
 
+            // The decorator itself doesn't handle loading and applying validators, just modifying events.
             loadModelValidators(validator.validatorFactory);
         }
 
-        private void loadMetadataSeverityOverrides() {
-            model.getMetadataProperty(SEVERITY_OVERRIDES).ifPresent(value -> {
-                try {
-                    List<ObjectNode> values = value.expectArrayNode().getElementsAs(ObjectNode.class);
-                    for (ObjectNode rule : values) {
-                        try {
-                            severityOverrides.add(SeverityOverride.fromMetadata(rule));
-                        } catch (SourceException e) {
-                            pushEvent(ValidationEvent.fromSourceException(e));
-                        }
-                    }
-                } catch (SourceException e) {
-                    pushEvent(ValidationEvent.fromSourceException(e));
-                }
-            });
-        }
-
-        private void loadMetadataSuppressions() {
-            model.getMetadataProperty(SUPPRESSIONS).ifPresent(value -> {
-                try {
-                    List<ObjectNode> values = value.expectArrayNode().getElementsAs(ObjectNode.class);
-                    for (ObjectNode rule : values) {
-                        try {
-                            suppressions.add(Suppression.fromMetadata(rule));
-                        } catch (SourceException e) {
-                            pushEvent(ValidationEvent.fromSourceException(e));
-                        }
-                    }
-                } catch (SourceException e) {
-                    pushEvent(ValidationEvent.fromSourceException(e));
-                }
-            });
-        }
-
         private void loadModelValidators(ValidatorFactory validatorFactory) {
             // Load validators defined in metadata.
             ValidatedResult<List<ValidatorDefinition>> loaded = ValidationLoader
@@ -291,8 +265,9 @@ final class ModelValidator implements Validator {
         }
 
         private void pushEvent(ValidationEvent event) {
-            event = modifyEventSeverity(event);
-            event = validationEventDecorator.decorate(event);
+            if (validationEventDecorator.canDecorate(event)) {
+                event = validationEventDecorator.decorate(event);
+            }
             events.add(event);
             eventListener.accept(event);
         }
@@ -310,8 +285,7 @@ final class ModelValidator implements Validator {
             List<ValidationEvent> result = validators.parallelStream()
                     .flatMap(validator -> validator.validate(model).stream())
                     .filter(this::filterPrelude)
-                    .map(this::modifyEventSeverity)
-                    .map(validationEventDecorator::decorate)
+                    .map(e -> validationEventDecorator.canDecorate(e) ? validationEventDecorator.decorate(e) : e)
                     // Emit events as they occur during validation.
                     .peek(eventListener)
                     .collect(Collectors.toList());
@@ -330,52 +304,5 @@ final class ModelValidator implements Validator {
                     .filter(Prelude::isPreludeShape)
                     .isPresent();
         }
-
-        private ValidationEvent modifyEventSeverity(ValidationEvent event) {
-            // Use a suppress trait if present.
-            if (event.getShapeId().isPresent()) {
-                ShapeId target = event.getShapeId().get();
-                Shape shape = model.getShape(target).orElse(null);
-                if (shape != null) {
-                    if (shape.hasTrait(SuppressTrait.class)) {
-                        Suppression suppression = Suppression.fromSuppressTrait(shape);
-                        if (suppression.test(event)) {
-                            return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null));
-                        }
-                    }
-                }
-            }
-
-            // Check metadata and manual suppressions.
-            for (Suppression suppression : suppressions) {
-                if (suppression.test(event)) {
-                    return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null));
-                }
-            }
-
-            Severity appliedSeverity = event.getSeverity();
-            for (SeverityOverride override : severityOverrides) {
-                Severity overrideResult = override.apply(event);
-                if (overrideResult.ordinal() > appliedSeverity.ordinal()) {
-                    appliedSeverity = overrideResult;
-                }
-            }
-
-            return changeSeverity(event, appliedSeverity, null);
-        }
-
-        private static ValidationEvent changeSeverity(ValidationEvent event, Severity severity, String reason) {
-            if (event.getSeverity() == severity) {
-                return event;
-            } else {
-                // The event was suppressed so change the severity and reason.
-                ValidationEvent.Builder builder = event.toBuilder();
-                builder.severity(severity);
-                if (reason != null) {
-                    builder.suppressionReason(reason);
-                }
-                return builder.build();
-            }
-        }
     }
 }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java
index 8cfba31d1..7090ea17f 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java
@@ -24,6 +24,20 @@ import java.util.List;
  * relevant only to the context where Smithy is being used.
  */
 public interface ValidationEventDecorator {
+
+    /** A decorator that does nothing. */
+    ValidationEventDecorator IDENTITY = new ValidationEventDecorator() {
+        @Override
+        public boolean canDecorate(ValidationEvent ev) {
+            return false;
+        }
+
+        @Override
+        public ValidationEvent decorate(ValidationEvent ev) {
+            return ev;
+        }
+    };
+
     /**
      * Returns true if this decorator knows how to decorate this event, usually by looking at the event id.
      *
@@ -48,17 +62,7 @@ public interface ValidationEventDecorator {
      */
     static ValidationEventDecorator compose(List<ValidationEventDecorator> decorators) {
         if (decorators.isEmpty()) {
-            return new ValidationEventDecorator() {
-                @Override
-                public boolean canDecorate(ValidationEvent ev) {
-                    return false;
-                }
-
-                @Override
-                public ValidationEvent decorate(ValidationEvent ev) {
-                    return ev;
-                }
-            };
+            return IDENTITY;
         } else {
             return new ValidationEventDecorator() {
                 @Override
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecorator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecorator.java
new file mode 100644
index 000000000..a90ce944c
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecorator.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 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.model.validation.suppressions;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.SourceException;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.traits.SuppressTrait;
+import software.amazon.smithy.model.validation.Severity;
+import software.amazon.smithy.model.validation.ValidatedResult;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidationEventDecorator;
+import software.amazon.smithy.utils.SmithyUnstableApi;
+
+/**
+ * Creates a {@link ValidationEventDecorator} that applies custom suppressions, custom severity overrides,
+ * suppressions parsed from model metadata, and severity overrides parsed from model metadata.
+ */
+@SmithyUnstableApi
+public final class ModelBasedEventDecorator {
+
+    private static final String SUPPRESSIONS = "suppressions";
+    private static final String SEVERITY_OVERRIDES = "severityOverrides";
+
+    private final List<Suppression> suppressions = new ArrayList<>();
+    private final List<SeverityOverride> severityOverrides = new ArrayList<>();
+
+    /**
+     * Sets custom suppressions not found in the model.
+     *
+     * @param suppressions Suppressions to set.
+     * @return Returns the ModelBasedEventDecorator.
+     */
+    public ModelBasedEventDecorator suppressions(Collection<? extends Suppression> suppressions) {
+        this.suppressions.clear();
+        this.suppressions.addAll(suppressions);
+        return this;
+    }
+
+    /**
+     * Sets custom severity overrides not found in the model.
+     *
+     * @param severityOverrides Severity overrides to set.
+     * @return Returns the ModelBasedEventDecorator.
+     */
+    public ModelBasedEventDecorator severityOverrides(Collection<? extends SeverityOverride> severityOverrides) {
+        this.severityOverrides.clear();
+        this.severityOverrides.addAll(severityOverrides);
+        return this;
+    }
+
+    /**
+     * Creates a ValidationEventDecorator for the given Model.
+     *
+     * <p>Validators, suppressions, and severity overrides found in the model are used each time
+     * {@link ValidationEventDecorator#decorate(ValidationEvent)} is called. The
+     * {@link ValidationEventDecorator#canDecorate(ValidationEvent)} always returns true.
+     *
+     * @param model Model to load validation events from.
+     * @return Returns a decorator that can be used to modify the severity and suppression reason of each given event.
+     */
+    public ValidatedResult<ValidationEventDecorator> createDecorator(Model model) {
+        // Create dedicated arrays to separate the state of the created decorator from the builder.
+        List<ValidationEvent> events = new ArrayList<>();
+        List<Suppression> loadedSuppressions = new ArrayList<>(suppressions);
+        loadMetadataSuppressions(model, loadedSuppressions, events);
+        List<SeverityOverride> loadedSeverityOverrides = new ArrayList<>(severityOverrides);
+        loadMetadataSeverityOverrides(model, loadedSeverityOverrides, events);
+
+        // Modify severities and overrides of each encountered event.
+        for (int i = 0; i < events.size(); i++) {
+            events.set(i, modifyEventSeverity(model, events.get(i), loadedSuppressions, loadedSeverityOverrides));
+        }
+
+        return new ValidatedResult<>(new ValidationEventDecorator() {
+            @Override
+            public boolean canDecorate(ValidationEvent ev) {
+                return true;
+            }
+
+            @Override
+            public ValidationEvent decorate(ValidationEvent ev) {
+                return modifyEventSeverity(model, ev, loadedSuppressions, loadedSeverityOverrides);
+            }
+        }, events);
+    }
+
+    private static void loadMetadataSeverityOverrides(
+            Model model,
+            List<SeverityOverride> severityOverrides,
+            List<ValidationEvent> events
+    ) {
+        model.getMetadataProperty(SEVERITY_OVERRIDES).ifPresent(value -> {
+            try {
+                List<ObjectNode> values = value.expectArrayNode().getElementsAs(ObjectNode.class);
+                for (ObjectNode rule : values) {
+                    try {
+                        severityOverrides.add(SeverityOverride.fromMetadata(rule));
+                    } catch (SourceException e) {
+                        events.add(ValidationEvent.fromSourceException(e));
+                    }
+                }
+            } catch (SourceException e) {
+                events.add(ValidationEvent.fromSourceException(e));
+            }
+        });
+    }
+
+    private static void loadMetadataSuppressions(
+            Model model,
+            List<Suppression> suppressions,
+            List<ValidationEvent> events
+    ) {
+        model.getMetadataProperty(SUPPRESSIONS).ifPresent(value -> {
+            try {
+                List<ObjectNode> values = value.expectArrayNode().getElementsAs(ObjectNode.class);
+                for (ObjectNode rule : values) {
+                    try {
+                        suppressions.add(Suppression.fromMetadata(rule));
+                    } catch (SourceException e) {
+                        events.add(ValidationEvent.fromSourceException(e));
+                    }
+                }
+            } catch (SourceException e) {
+                events.add(ValidationEvent.fromSourceException(e));
+            }
+        });
+    }
+
+    private static ValidationEvent modifyEventSeverity(
+            Model model,
+            ValidationEvent event,
+            List<Suppression> suppressions,
+            List<SeverityOverride> severityOverrides
+    ) {
+        // Use a suppress trait if present.
+        if (event.getShapeId().isPresent()) {
+            ShapeId target = event.getShapeId().get();
+            Shape shape = model.getShape(target).orElse(null);
+            if (shape != null) {
+                if (shape.hasTrait(SuppressTrait.class)) {
+                    Suppression suppression = Suppression.fromSuppressTrait(shape);
+                    if (suppression.test(event)) {
+                        return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null));
+                    }
+                }
+            }
+        }
+
+        // Check metadata and manual suppressions.
+        for (Suppression suppression : suppressions) {
+            if (suppression.test(event)) {
+                return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null));
+            }
+        }
+
+        Severity appliedSeverity = event.getSeverity();
+        for (SeverityOverride override : severityOverrides) {
+            Severity overrideResult = override.apply(event);
+            if (overrideResult.ordinal() > appliedSeverity.ordinal()) {
+                appliedSeverity = overrideResult;
+            }
+        }
+
+        return changeSeverity(event, appliedSeverity, null);
+    }
+
+    private static ValidationEvent changeSeverity(ValidationEvent event, Severity severity, String reason) {
+        if (event.getSeverity() == severity) {
+            return event;
+        } else {
+            // The event was suppressed so change the severity and reason.
+            ValidationEvent.Builder builder = event.toBuilder();
+            builder.severity(severity);
+            if (reason != null) {
+                builder.suppressionReason(reason);
+            }
+            return builder.build();
+        }
+    }
+}
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 2fc4b2c42..54da6f88b 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
@@ -1,6 +1,13 @@
+// This model defines the Smithy prelude model.
+//
+// Shapes in the prelude model that are not marked as `@private` can be
+// referenced using a relative shape ID.
 $version: "2.0"
+
 namespace smithy.api
 
+// --- The following shapes are publicly available prelude shapes.
+
 string String
 
 blob Blob
@@ -27,9 +34,6 @@ float Float
 
 double Double
 
-@unitType
-structure Unit {}
-
 @default(false)
 boolean PrimitiveBoolean
 
@@ -51,25 +55,36 @@ float PrimitiveFloat
 @default(0)
 double PrimitiveDouble
 
+// The `Unit` type is a publicly available "unit" value type. This shape
+// is the only shape in Smithy allowed to be marked with the `@unitType` trait.
+@unitType
+structure Unit {}
+
+// --- Shapes below are traits and the private shapes that define them.
+
 /// Makes a shape a trait.
 @trait(
-    selector: ":is(simpleType, list, map, structure, union)",
+    selector: ":is(simpleType, list, map, structure, union)"
     breakingChanges: [
-        {change: "presence"},
-        {path: "/structurallyExclusive", change: "any"},
-        {path: "/conflicts", change: "update", severity: "NOTE", message:
-            "Adding more conflicts to a trait could cause previously written models to fail validation."}
+        {change: "presence"}
+        {path: "/structurallyExclusive", change: "any"}
+        {
+            path: "/conflicts"
+            change: "update"
+            severity: "NOTE"
+            message: "Adding more conflicts to a trait could cause previously written models to fail validation."
+        }
     ]
 )
 structure trait {
     /// The valid places in a model that the trait can be applied.
-    selector: String,
+    selector: String
 
     /// Whether or not only a single member in a shape can have this trait.
-    structurallyExclusive: StructurallyExclusive,
+    structurallyExclusive: StructurallyExclusive
 
     /// The traits that this trait conflicts with.
-    conflicts: NonEmptyStringList,
+    conflicts: NonEmptyStringList
 
     /// Defines the backward compatibility rules of the trait.
     breakingChanges: TraitDiffRules
@@ -84,14 +99,14 @@ list TraitDiffRules {
 @private
 structure TraitDiffRule {
     /// Defines a JSON Pointer to the value to evaluate.
-    path: String,
+    path: String
 
     /// Defines the type of change that is not allowed.
     @required
-    change: TraitChangeType,
+    change: TraitChangeType
 
     /// Defines the severity of the change. Defaults to ERROR if not defined.
-    severity: TraitChangeSeverity = "ERROR",
+    severity: TraitChangeSeverity = "ERROR"
 
     /// Provides a reason why the change is potentially backward incompatible.
     message: String
@@ -99,10 +114,10 @@ structure TraitDiffRule {
 
 @private
 enum TraitChangeType {
-    /// Emit when a trait already existed, continues to exist, but it is modified.
+    /// Emit when a trait is modified.
     UPDATE = "update"
 
-    /// Emit when a trait or value is added that previously did not exist
+    /// Emit when a trait or value is added that previously did not exist.
     ADD = "add"
 
     /// Emit when a trait or value is removed.
@@ -135,7 +150,8 @@ enum StructurallyExclusive {
     /// Only a single member of a shape can be marked with the trait.
     MEMBER = "member"
 
-    /// Only a single member of a shape can target a shape marked with this trait.
+    /// Only a single member of a shape can target a shape marked with
+    /// this trait.
     TARGET = "target"
 }
 
@@ -143,21 +159,22 @@ enum StructurallyExclusive {
 @trait
 structure deprecated {
     /// The reason for deprecation.
-    message: String,
+    message: String
 
-    /// A description of when the shape was deprecated (e.g., a date or version).
-    since: String,
+    /// A description of when the shape was deprecated (e.g., a date or
+    /// version).
+    since: String
 }
 
 /// Used only in Smithy 1.0 to indicate that a shape is boxed.
-/// This trait cannot be used in Smithy 2.0 models.
 ///
-/// When a boxed shape is the target of a member, the member
-/// may or may not contain a value, and the member has no default value.
+/// This trait cannot be used in Smithy 2.0 models. When a boxed shape is the
+/// target of a member, the member may or may not contain a value, and the
+/// member has no default value.
 @trait(
     selector: """
         :test(boolean, byte, short, integer, long, float, double,
-            member > :test(boolean, byte, short, integer, long, float, double))"""
+        member > :test(boolean, byte, short, integer, long, float, double))"""
 )
 structure box {}
 
@@ -169,11 +186,11 @@ string documentation
 @trait
 @length(min: 1)
 map externalDocumentation {
-    key: NonEmptyString,
+    key: NonEmptyString
     value: NonEmptyString
 }
 
-/// Defines the list of authentication schemes supported by a service or operation.
+/// Defines the ordered list of supported authentication schemes.
 @trait(selector: ":is(service, operation)")
 @uniqueItems
 list auth {
@@ -190,21 +207,23 @@ string AuthTraitReference
 /// The targeted trait must only be applied to service shapes, must be a
 /// structure, and must have the `trait` trait.
 @trait(
-    selector: "structure[trait|trait]",
-    breakingChanges: [{change: "presence"}]
+    selector: "structure[trait|trait]"
+    breakingChanges: [
+        {change: "presence"}
+    ]
 )
 structure protocolDefinition {
-    /// Defines a list of traits that protocol implementations must
-    /// understand in order to successfully use the protocol.
-    traits: TraitShapeIdList,
+    /// The list of traits that protocol implementations must understand in
+    /// order to successfully use the protocol.
+    traits: TraitShapeIdList
 
     /// Set to true if inline documents are not supported by this protocol.
-    noInlineDocumentSupport: Boolean,
+    noInlineDocumentSupport: Boolean
 }
 
 @private
 list TraitShapeIdList {
-    member: TraitShapeId,
+    member: TraitShapeId
 }
 
 @private
@@ -216,88 +235,98 @@ string TraitShapeId
 /// The targeted trait must only be applied to service shapes or operation
 /// shapes, must be a structure, and must have the `trait` trait.
 @trait(
-    selector: "structure[trait|trait]",
-    breakingChanges: [{change: "presence"}]
+    selector: "structure[trait|trait]"
+    breakingChanges: [
+        {change: "presence"}
+    ]
 )
 structure authDefinition {
-    /// Defines a list of traits that auth implementations must
-    /// understand in order to successfully use the scheme.
-    traits: TraitShapeIdList,
+    /// The list of traits that auth implementations must understand in order
+    /// to successfully use the scheme.
+    traits: TraitShapeIdList
 }
 
-/// Enables HTTP Basic Authentication as defined in RFC 2617
-/// on a service or operation.
+/// Enables HTTP Basic Authentication as defined in RFC 2617 on a service
+/// or operation.
 @trait(
-    selector: "service",
-    breakingChanges: [{change: "remove"}]
+    selector: "service"
+    breakingChanges: [
+        {change: "remove"}
+    ]
 )
 @authDefinition
 @externalDocumentation("RFC 2617": "https://tools.ietf.org/html/rfc2617.html")
 structure httpBasicAuth {}
 
-/// Enables HTTP Digest Authentication as defined in RFC 2617
-/// on a service or operation.
+/// Enables HTTP Digest Authentication as defined in RFC 2617 on a service
+/// or operation.
 @trait(
-    selector: "service",
-    breakingChanges: [{change: "remove"}]
+    selector: "service"
+    breakingChanges: [
+        {change: "remove"}
+    ]
 )
 @authDefinition
 @externalDocumentation("RFC 2617": "https://tools.ietf.org/html/rfc2617.html")
 structure httpDigestAuth {}
 
-/// Enables HTTP Bearer Authentication as defined in RFC 6750
-/// on a service or operation.
+/// Enables HTTP Bearer Authentication as defined in RFC 6750 on a service
+/// or operation.
 @trait(
-    selector: "service",
-    breakingChanges: [{change: "remove"}]
+    selector: "service"
+    breakingChanges: [
+        {change: "remove"}
+    ]
 )
 @authDefinition
 @externalDocumentation("RFC 6750": "https://tools.ietf.org/html/rfc6750.html")
 structure httpBearerAuth {}
 
-/// An HTTP-specific authentication scheme that sends an arbitrary
-/// API key in a header or query string parameter.
+/// An HTTP-specific authentication scheme that sends an arbitrary API key in
+/// a header or query string parameter.
 @trait(
-    selector: "service",
-    breakingChanges: [{change: "remove"}]
+    selector: "service"
+    breakingChanges: [
+        {change: "remove"}
+    ]
 )
 @authDefinition
 structure httpApiKeyAuth {
-    /// Defines the name of the HTTP header or query string parameter
-    /// that contains the API key.
+    /// Defines the name of the HTTP header or query string parameter that
+    /// contains the API key.
     @required
-    name: NonEmptyString,
+    name: NonEmptyString
 
-    /// Defines the location of where the key is serialized. This value
-    /// can be set to `"header"` or `"query"`.
+    /// Defines the location of where the key is serialized. This value can
+    /// be set to `"header"` or `"query"`.
     @required
-    in: HttpApiKeyLocations,
+    in: HttpApiKeyLocations
 
-    /// Defines the security scheme to use on the ``Authorization`` header value
+    /// Defines the security scheme to use in the ``Authorization`` header.
     /// This can only be set if the "in" property is set to ``header``.
-    scheme: NonEmptyString,
+    scheme: NonEmptyString
 }
 
-/// Provides a structure member with a default value. When added to root level shapes, requires that every
-/// targeting structure member defines the same default value on the member or sets a default of null.
+/// Provides a structure member with a default value. When added to root
+/// level shapes, requires that every targeting structure member defines the
+/// same default value on the member or sets a default of null.
 ///
 /// This trait can currently only be used in Smithy 2.0 models.
-@trait(
-    selector: ":is(simpleType, list, map, structure > member :test(> :is(simpleType, list, map)))"
-)
+@trait(selector: ":is(simpleType, list, map, structure > member :test(> :is(simpleType, list, map)))")
 document default
 
 /// Indicates that the default trait was added to a member.
 @trait(
-    selector: "structure > member [trait|default]",
+    selector: "structure > member [trait|default]"
     breakingChanges: [
         {change: "remove"}
     ]
 )
 structure addedDefault {}
 
-/// Requires that non-authoritative generators like clients treat a structure member as
-/// nullable regardless of if the member is also marked with the required trait.
+/// Requires that non-authoritative generators like clients treat a structure
+/// member as nullable regardless of if the member is also marked with the
+/// required trait.
 @trait(selector: "structure > member")
 structure clientOptional {}
 
@@ -309,47 +338,51 @@ enum HttpApiKeyLocations {
 
 /// Indicates that an operation can be called without authentication.
 @trait(
-    selector: "operation",
-    breakingChanges: [{change: "remove"}]
+    selector: "operation"
+    breakingChanges: [
+        {change: "remove"}
+    ]
 )
 structure optionalAuth {}
 
 /// Provides example inputs and outputs for operations.
 @trait(selector: "operation")
 list examples {
-    member: Example,
+    member: Example
 }
 
 @private
 structure Example {
     @required
-    title: String,
+    title: String
 
-    documentation: String,
+    documentation: String
 
-    input: Document,
+    input: Document
 
-    output: Document,
+    output: Document
 
-    error: ExampleError,
+    error: ExampleError
 }
 
 @private
 structure ExampleError {
     @idRef(selector: "structure[trait|error]")
-    shapeId: String,
+    shapeId: String
 
-    content: Document,
+    content: Document
 }
 
 /// Indicates that a structure shape represents an error.
 ///
-/// All shapes referenced by the errors list of an operation MUST be
-/// targeted with this trait.
+/// All shapes referenced by the errors list of an operation MUST be targeted
+/// with this trait.
 @trait(
-    selector: "structure",
-    conflicts: [trait],
-    breakingChanges: [{change: "any"}]
+    selector: "structure"
+    conflicts: [trait]
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 enum error {
     CLIENT = "client"
@@ -358,112 +391,139 @@ enum error {
 
 /// Indicates that an error MAY be retried by the client.
 @trait(
-    selector: "structure[trait|error]",
-    breakingChanges: [{change: "remove"}]
+    selector: "structure[trait|error]"
+    breakingChanges: [
+        {change: "remove"}
+    ]
 )
 structure retryable {
     /// Classifies the retry as throttling.
-    throttling: Boolean,
+    throttling: Boolean
 }
 
 /// Indicates that an operation is effectively read-only.
 @trait(
-    selector: "operation",
-    conflicts: [idempotent],
-    breakingChanges: [{change: "remove"}]
+    selector: "operation"
+    conflicts: [idempotent]
+    breakingChanges: [
+        {change: "remove"}
+    ]
 )
 structure readonly {}
 
 /// Indicates that the intended effect on the server of multiple identical
-/// requests with an operation is the same as the effect for a single
-/// such request.
+/// requests with an operation is the same as the effect for a single such
+/// request.
 @trait(
-    selector: "operation",
-    conflicts: [readonly],
-    breakingChanges: [{change: "remove"}]
+    selector: "operation"
+    conflicts: [readonly]
+    breakingChanges: [
+        {change: "remove"}
+    ]
 )
 structure idempotent {}
 
 /// Defines the input member of an operation that is used by the server to
 /// identify and discard replayed requests.
 @trait(
-    selector: "structure > :test(member > string)",
-    structurallyExclusive: "member",
-    breakingChanges: [{change: "remove"}]
+    selector: "structure > :test(member > string)"
+    structurallyExclusive: "member"
+    breakingChanges: [
+        {change: "remove"}
+    ]
 )
 @notProperty
 structure idempotencyToken {}
 
 /// Shapes marked with the internal trait are meant only for internal use and
 /// must not be exposed to customers.
-@trait(breakingChanges: [{
-    change: "remove",
-    severity: "WARNING",
-    message: "Removing the @internal trait makes a shape externally visible."}
-])
+@trait(
+    breakingChanges: [
+        {
+            change: "remove"
+            severity: "WARNING"
+            message: "Removing the @internal trait makes a shape externally visible."
+        }
+    ]
+)
 structure internal {}
 
-/// The jsonName trait allows a serialized object property name to differ
-/// from a structure member name used in the model.
+/// Allows a serialized object property name to differ from a structure member
+/// name used in the model.
 @trait(
-    selector: ":is(structure, union) > member",
-    breakingChanges: [{change: "any"}]
+    selector: ":is(structure, union) > member"
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 string jsonName
 
-/// Serializes an object property as an XML attribute rather than a nested XML element.
+/// Serializes an object property as an XML attribute rather than a nested XML
+/// element.
 @trait(
-    selector: "structure > :test(member > :test(boolean, number, string, timestamp))",
-    conflicts: [xmlNamespace],
-    breakingChanges: [{change: "any"}]
+    selector: "structure > :test(member > :test(boolean, number, string, timestamp))"
+    conflicts: [xmlNamespace]
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 structure xmlAttribute {}
 
-/// Unwraps the values of a list, set, or map into the containing structure/union.
+/// Unwraps the values of a list, set, or map into the containing
+/// structure/union.
 @trait(
-    selector: ":is(structure, union) > :test(member > :test(list, map))",
-    breakingChanges: [{change: "any"}]
+    selector: ":is(structure, union) > :test(member > :test(list, map))"
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 structure xmlFlattened {}
 
-/// Changes the serialized element or attribute name of a structure, union, or member.
+/// Changes the serialized element or attribute name of a structure, union,
+/// or member.
 @trait(
-    selector: ":is(structure, union, member)",
-    breakingChanges: [{change: "any"}]
+    selector: ":is(structure, union, member)"
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 @pattern("^[a-zA-Z_][a-zA-Z_0-9-]*(:[a-zA-Z_][a-zA-Z_0-9-]*)?$")
 string xmlName
 
 /// Adds an xmlns namespace definition URI to an XML element.
 @trait(
-    selector: ":is(service, member, simpleType, list, map, structure, union)",
-    conflicts: [xmlAttribute],
-    breakingChanges: [{change: "any"}]
+    selector: ":is(service, member, simpleType, list, map, structure, union)"
+    conflicts: [xmlAttribute]
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 structure xmlNamespace {
     /// The namespace URI for scoping this XML element.
     @required
-    uri: NonEmptyString,
+    uri: NonEmptyString
+
     /// The prefix for the given namespace.
     @pattern("^[a-zA-Z_][a-zA-Z_0-9-]*$")
-    prefix: NonEmptyString,
+    prefix: NonEmptyString
 }
 
 @private
 @length(min: 1)
 string NonEmptyString
 
-/// Indicates that the put lifecycle operation of a resource
-/// can only be used to create a resource and cannot replace
-/// an existing resource.
+/// Indicates that the put lifecycle operation of a resource can only be used
+/// to create a resource and cannot replace an existing resource.
 @trait(selector: "resource:test(-[put]->)")
 structure noReplace {}
 
 /// Describes the contents of a blob shape using a media type as defined by
 /// RFC 6838 (e.g., "video/quicktime").
 @trait(
-    selector: ":is(blob, string)",
-    breakingChanges: [{change: "remove"}]
+    selector: ":is(blob, string)"
+    breakingChanges: [
+        {change: "remove"}
+    ]
 )
 string mediaType
 
@@ -479,50 +539,56 @@ list references {
 structure Reference {
     /// The shape ID of the referenced resource.
     @required
-    resource: NonEmptyString,
+    resource: NonEmptyString
 
     /// Defines a mapping of each resource identifier name to a structure member
     /// name that provides its value. Each key in the map MUST refer to one of the
     /// identifier names in the identifiers property of the resource, and each
     /// value in the map MUST refer to a valid structure member name that targets
     /// a string shape.
-    ids: NonEmptyStringMap,
+    ids: NonEmptyStringMap
 
     /// Providing a service makes the reference specific to a particular binding
     /// of the resource to a service. When omitted, the reference is late-bound to
     /// a service, meaning the reference is assumed to be a reference to the
     /// resource bound to the service currently in use by the client or server.
-    service: NonEmptyString,
+    service: NonEmptyString
 
     /// Defines the semantics of the relationship. The rel property SHOULD
     /// contain a link relation as defined in RFC 5988#section-4.
-    rel: NonEmptyString,
+    rel: NonEmptyString
 }
 
 @private
 map NonEmptyStringMap {
-    key: NonEmptyString,
+    key: NonEmptyString
     value: NonEmptyString
 }
 
-/// Indicates that the targeted structure member provides an identifier for a resource.
+/// Indicates that the targeted structure member provides an identifier for
+/// a resource.
 @trait(
-    selector: "structure > :test(member[trait|required] > string)",
-    breakingChanges: [{change: "remove"}]
+    selector: "structure > :test(member[trait|required] > string)"
+    breakingChanges: [
+        {change: "remove"}
+    ]
 )
 @length(min: 1)
 @notProperty
 string resourceIdentifier
 
-/// Prevents models defined in a different namespace from referencing the targeted shape.
+/// Prevents models defined in a different namespace from referencing the
+/// targeted shape.
 @trait
 structure private {}
 
-/// Indicates that the data stored in the shape is sensitive and MUST be handled with care.
+/// Indicates that the data stored in the shape is sensitive and MUST be
+/// handled with care.
 @trait(selector: ":not(:test(service, operation, resource, member))")
 structure sensitive {}
 
-/// Defines the version or date in which a shape or member was added to the model.
+/// Defines the version or date in which a shape or member was added to the
+/// model.
 @trait
 string since
 
@@ -531,16 +597,20 @@ string since
 /// unknown at the start of a request. If the target is a union then the shape
 /// represents a stream of events.
 @trait(
-    selector: ":is(blob, union)",
-    structurallyExclusive: "target",
-    breakingChanges: [{change: "any"}]
+    selector: ":is(blob, union)"
+    structurallyExclusive: "target"
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 structure streaming {}
 
 /// Indicates that the streaming blob must be finite and has a known size.
 @trait(
-    selector: "blob[trait|streaming]",
-    breakingChanges: [{change: "presence"}]
+    selector: "blob[trait|streaming]"
+    breakingChanges: [
+        {change: "presence"}
+    ]
 )
 structure requiresLength {}
 
@@ -562,9 +632,10 @@ string title
 /// Constrains the acceptable values of a string to a fixed set
 /// of constant values.
 @trait(
-    selector: "string :not(enum)",
-    // It's a breaking change to change values or enums or the ordering of enums,
-    // but that validation happens in code to provide better error messages.
+    selector: "string :not(enum)"
+    // It's a breaking change to change values or enums or the ordering of
+    // enums, but that validation happens in code to provide better error
+    // messages.
     breakingChanges: [
         {change: "presence"}
     ]
@@ -580,19 +651,19 @@ list enum {
 structure EnumDefinition {
     /// Defines the enum value that is sent over the wire.
     @required
-    value: NonEmptyString,
+    value: NonEmptyString
 
-    /// Defines the name, or label, that is used in code to represent this variant.
-    name: EnumConstantBodyName,
+    /// Defines the name that is used in code to represent this variant.
+    name: EnumConstantBodyName
 
     /// Provides optional documentation about the enum constant value.
-    documentation: String,
+    documentation: String
 
     /// Applies a list of tags to the enum constant.
-    tags: NonEmptyStringList,
+    tags: NonEmptyStringList
 
     /// Whether the enum value should be considered deprecated.
-    deprecated: Boolean,
+    deprecated: Boolean
 }
 
 /// The optional name or label of the enum constant value.
@@ -612,92 +683,109 @@ document enumValue
 @trait(selector: ":test(list, map, string, blob, member > :is(list, map, string, blob))")
 structure length {
     /// Integer value that represents the minimum inclusive length of a shape.
-    min: Long,
+    min: Long
 
     /// Integer value that represents the maximum inclusive length of a shape.
-    max: Long,
+    max: Long
 }
 
 /// Restricts allowed values of byte, short, integer, long, float, double,
-/// bigDecimal, and bigInteger shapes within an acceptable lower and upper bound.
+/// bigDecimal, and bigInteger shapes within an acceptable lower and upper
+/// bound.
 @trait(selector: ":test(number, member > number)")
 structure range {
     /// Specifies the allowed inclusive minimum value.
-    min: BigDecimal,
+    min: BigDecimal
 
     /// Specifies the allowed inclusive maximum value.
-    max: BigDecimal,
+    max: BigDecimal
 }
 
 /// Restricts string shape values to a specified regular expression.
 @trait(
-    selector: ":test(string, member > string)",
+    selector: ":test(string, member > string)"
     breakingChanges: [
         {
-            change: "add",
-            severity: "WARNING",
+            change: "add"
+            severity: "WARNING"
             message: "The @pattern trait should only be added if the string already had adhered to the pattern."
-        },
+        }
         {
-            change: "update",
-            severity: "NOTE",
+            change: "update"
+            severity: "NOTE"
             message: "Changes to the @pattern trait should generally make the string more permissive, not less."
         }
     ]
 )
 string pattern
 
-/// Marks a structure member as required, meaning a value for the member MUST be present.
+/// Marks a structure member as required, meaning a value for the member MUST
+/// be present.
 @trait(selector: "structure > member")
 structure required {}
 
 /// Configures a structure member's resource property mapping behavior.
 @trait(
-    selector: "structure > member",
-    conflicts: [resourceIdentifier],
-    breakingChanges: [{change: "remove"}, {change: "update"}]
+    selector: "structure > member"
+    conflicts: [resourceIdentifier]
+    breakingChanges: [
+        {change: "remove"}
+        {change: "update"}
+    ]
 )
 structure property {
     name: String
 }
 
-/// Explicitly excludes a member from resource property mapping or
-/// enables another trait to carry the same implied meaning.
+/// Explicitly excludes a member from resource property mapping or enables
+/// another trait to carry the same implied meaning.
 @trait(
-    selector: ":is(operation -[input, output]-> structure > member, [trait|trait])",
-    breakingChanges: [{change: "add"}]
+    selector: ":is(operation -[input, output]-> structure > member, [trait|trait])"
+    breakingChanges: [
+        {change: "add"}
+    ]
 )
 @notProperty
 structure notProperty {}
 
-/// Adjusts the resource property mapping of a lifecycle operation to the targeted member.
+/// Adjusts the resource property mapping of a lifecycle operation to the
+/// targeted member.
 @trait(
     selector: "operation -[input, output]-> structure > member :test(> structure)"
     structurallyExclusive: "member"
-    breakingChanges: [{change: "any"}]
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 @notProperty
 structure nestedProperties {}
 
 /// Indicates that a structure member SHOULD be set.
-@trait(selector: "structure > member", conflicts: [required])
+@trait(
+    selector: "structure > member"
+    conflicts: [required]
+)
 structure recommended {
     /// Provides a reason why the member is recommended.
-    reason: String,
+    reason: String
 }
 
 /// Marks a list or map as sparse.
 @trait(
-    selector: ":is(list, map)",
-    breakingChanges: [{change: "presence"}]
+    selector: ":is(list, map)"
+    breakingChanges: [
+        {change: "presence"}
+    ]
 )
 structure sparse {}
 
 /// Indicates that the items in a list MUST be unique.
 @trait(
-    selector: "list :not(> member ~> :is(float, double, document))",
-    conflicts: [sparse],
-    breakingChanges: [{change: "presence", severity: "WARNING"}]
+    selector: "list :not(> member ~> :is(float, double, document))"
+    conflicts: [sparse]
+    breakingChanges: [
+        {change: "presence", severity: "WARNING"}
+    ]
 )
 structure uniqueItems {}
 
@@ -705,83 +793,92 @@ structure uniqueItems {}
 @trait
 structure unstable {}
 
-/// The paginated trait indicates that an operation intentionally limits the number
-/// of results returned in a single response and that multiple invocations might be
-/// necessary to retrieve all results.
+/// The paginated trait indicates that an operation intentionally limits the
+/// number of results returned in a single response and that multiple
+/// invocations might be necessary to retrieve all results.
 @trait(
-    selector: ":is(service, operation)",
+    selector: ":is(service, operation)"
     breakingChanges: [
-        {change: "remove"},
-        {path: "/inputToken",  change: "update"},
-        {path: "/outputToken", change: "update"},
-        {path: "/items",       change: "remove"},
-        {path: "/items",       change: "add",   severity: "NOTE"},
-        {path: "/items",       change: "update",   severity: "NOTE"},
-        {path: "/pageSize",    change: "update"},
-        {path: "/pageSize",    change: "remove"},
+        {change: "remove"}
+        {path: "/inputToken", change: "update"}
+        {path: "/outputToken", change: "update"}
+        {path: "/items", change: "remove"}
+        {path: "/items", change: "add", severity: "NOTE"}
+        {path: "/items", change: "update", severity: "NOTE"}
+        {path: "/pageSize", change: "update"}
+        {path: "/pageSize", change: "remove"}
     ]
 )
 structure paginated {
-    /// The name of the operation input member that represents the continuation token.
+    /// The name of the operation input member that represents the continuation
+    /// token.
     ///
-    /// When this value is provided as operation input, the service returns results
-    /// from where the previous response left off. This input member MUST NOT be
-    /// required and MUST target a string shape.
-    inputToken: NonEmptyString,
+    /// When this value is provided as operation input, the service returns
+    /// results from where the previous response left off. This input member
+    /// MUST NOT be required and MUST target a string shape.
+    inputToken: NonEmptyString
 
-    /// The name of the operation output member that represents the continuation token.
+    /// The name of the operation output member that represents the
+    /// continuation token.
     ///
-    /// When this value is present in operation output, it indicates that there are more
-    /// results to retrieve. To get the next page of results, the client uses the output
-    /// token as the input token of the next request. This output member MUST NOT be
-    /// required and MUST target a string shape.
-    outputToken: NonEmptyString,
+    /// When this value is present in operation output, it indicates that there
+    /// are more results to retrieve. To get the next page of results, the
+    /// client uses the output token as the input token of the next request.
+    /// This output member MUST NOT be required and MUST target a string shape.
+    outputToken: NonEmptyString
 
     /// The name of a top-level output member of the operation that is the data
     /// that is being paginated across many responses.
     ///
     /// The named output member, if specified, MUST target a list or map.
-    items: NonEmptyString,
+    items: NonEmptyString
 
     /// The name of an operation input member that limits the maximum number of
-    /// results to include in the operation output. This input member MUST NOT be
-    /// required and MUST target an integer shape.
-    pageSize: NonEmptyString,
+    /// results to include in the operation output. This input member MUST NOT
+    /// be required and MUST target an integer shape.
+    pageSize: NonEmptyString
 }
 
 /// Configures the HTTP bindings of an operation.
 @trait(
-    selector: "operation",
+    selector: "operation"
     breakingChanges: [
-        {change: "remove"},
-        {path: "/method", change: "update"},
-        {path: "/uri", change: "update"},
-        {path: "/code", change: "update"},
-        {path: "/code", change: "presence", severity: "DANGER", message: "Adding or removing is backward compatible only if the value is the default value of 200"}
+        {change: "remove"}
+        {path: "/method", change: "update"}
+        {path: "/uri", change: "update"}
+        {path: "/code", change: "update"}
+        {
+            path: "/code"
+            change: "presence"
+            severity: "DANGER"
+            message: "Adding or removing is backward compatible only if the value is the default value of 200"
+        }
     ]
 )
 structure http {
     /// The HTTP method of the operation.
     @required
-    method: NonEmptyString,
+    method: NonEmptyString
 
     /// The URI pattern of the operation.
     ///
     /// Labels defined in the URI pattern are used to bind operation input
     /// members to the URI.
     @required
-    uri: NonEmptyString,
+    uri: NonEmptyString
 
-    /// The HTTP status code of a successful response.
+    // The HTTP status code of a successful response.
     @range(min: 100, max: 999)
-    code: Integer = 200,
+    code: Integer = 200
 }
 
 /// Binds an operation input structure member to an HTTP label.
 @trait(
-    selector: "structure > member[trait|required] :test(> :test(string, number, boolean, timestamp))",
-    conflicts: [httpHeader, httpQuery, httpPrefixHeaders, httpPayload, httpResponseCode, httpQueryParams],
-    breakingChanges: [{change: "presence"}]
+    selector: "structure > member[trait|required] :test(> :test(string, number, boolean, timestamp))"
+    conflicts: [httpHeader, httpQuery, httpPrefixHeaders, httpPayload, httpResponseCode, httpQueryParams]
+    breakingChanges: [
+        {change: "presence"}
+    ]
 )
 structure httpLabel {}
 
@@ -790,9 +887,11 @@ structure httpLabel {}
     selector: """
         structure > member
         :test(> :test(string, number, boolean, timestamp),
-              > list > member > :test(string, number, boolean, timestamp))""",
-    conflicts: [httpLabel, httpHeader, httpPrefixHeaders, httpPayload, httpResponseCode, httpQueryParams],
-    breakingChanges: [{change: "any"}]
+        > list > member > :test(string, number, boolean, timestamp))"""
+    conflicts: [httpLabel, httpHeader, httpPrefixHeaders, httpPayload, httpResponseCode, httpQueryParams]
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 @length(min: 1)
 string httpQuery
@@ -801,10 +900,12 @@ string httpQuery
 @trait(
     selector: """
         structure > member
-        :test(> map > member[id|member=value] > :test(string, list > member > string))""",
-        structurallyExclusive: "member",
-    conflicts: [httpLabel, httpQuery, httpHeader, httpPayload, httpResponseCode, httpPrefixHeaders],
-    breakingChanges: [{change: "any"}]
+        :test(> map > member[id|member=value] > :test(string, list > member > string))"""
+    structurallyExclusive: "member"
+    conflicts: [httpLabel, httpQuery, httpHeader, httpPayload, httpResponseCode, httpPrefixHeaders]
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 structure httpQueryParams {}
 
@@ -812,9 +913,11 @@ structure httpQueryParams {}
 @trait(
     selector: """
         structure > :test(member > :test(boolean, number, string, timestamp,
-                list > member > :test(boolean, number, string, timestamp)))""",
-    conflicts: [httpLabel, httpQuery, httpPrefixHeaders, httpPayload, httpResponseCode, httpQueryParams],
-    breakingChanges: [{change: "any"}]
+        list > member > :test(boolean, number, string, timestamp)))"""
+    conflicts: [httpLabel, httpQuery, httpPrefixHeaders, httpPayload, httpResponseCode, httpQueryParams]
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 @length(min: 1)
 string httpHeader
@@ -823,26 +926,32 @@ string httpHeader
 @trait(
     selector: """
         structure > member
-        :test(> map :not([trait|sparse]) > member[id|member=value] > string)""",
-    structurallyExclusive: "member",
-    conflicts: [httpLabel, httpQuery, httpHeader, httpPayload, httpResponseCode, httpQueryParams],
-    breakingChanges: [{change: "any"}]
+        :test(> map :not([trait|sparse]) > member[id|member=value] > string)"""
+    structurallyExclusive: "member"
+    conflicts: [httpLabel, httpQuery, httpHeader, httpPayload, httpResponseCode, httpQueryParams]
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 string httpPrefixHeaders
 
 /// Binds a single structure member to the body of an HTTP request.
 @trait(
-    selector: "structure > member",
-    conflicts: [httpLabel, httpQuery, httpHeader, httpPrefixHeaders, httpResponseCode, httpQueryParams],
-    structurallyExclusive: "member",
-    breakingChanges: [{change: "presence"}]
+    selector: "structure > member"
+    conflicts: [httpLabel, httpQuery, httpHeader, httpPrefixHeaders, httpResponseCode, httpQueryParams]
+    structurallyExclusive: "member"
+    breakingChanges: [
+        {change: "presence"}
+    ]
 )
 structure httpPayload {}
 
 /// Defines an HTTP response code for an operation error.
 @trait(
-    selector: "structure[trait|error]",
-    breakingChanges: [{change: "any"}]
+    selector: "structure[trait|error]"
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 integer httpError
 
@@ -850,35 +959,40 @@ integer httpError
 /// status code. The value MAY differ from the HTTP status code provided
 /// on the response.
 @trait(
-    selector: "structure :not([trait|input]) > member :test(> integer)",
-    structurallyExclusive: "member",
-    conflicts: [httpLabel, httpQuery, httpHeader, httpPrefixHeaders, httpPayload, httpQueryParams],
-    breakingChanges: [{change: "any"}]
+    selector: "structure :not([trait|input]) > member :test(> integer)"
+    structurallyExclusive: "member"
+    conflicts: [httpLabel, httpQuery, httpHeader, httpPrefixHeaders, httpPayload, httpQueryParams]
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 structure httpResponseCode {}
 
 /// Defines how a service supports cross-origin resource sharing.
 @trait(
-    selector: "service",
-    breakingChanges: [{change: "remove"}]
+    selector: "service"
+    breakingChanges: [
+        {change: "remove"}
+    ]
 )
 structure cors {
-    /// The origin from which browser script-originating requests will be allowed.
-    origin: NonEmptyString = "*",
+    /// The origin from which browser script-originating requests will be
+    /// allowed.
+    origin: NonEmptyString = "*"
 
     /// The maximum number of seconds for which browsers are allowed to cache
     /// the results of a preflight OPTIONS request.
     ///
     /// Defaults to 600, the maximum age permitted by several browsers.
     /// Set to -1 to disable caching entirely.
-    maxAge: Integer = 600,
+    maxAge: Integer = 600
 
     /// The names of headers that should be included in the
     /// Access-Control-Allow-Headers header in responses to preflight OPTIONS
     /// requests. This list will be used in addition to the names of all
     /// request headers bound to an input data member via the httpHeader, as
     /// well as any headers required by the protocol or authentication scheme.
-    additionalAllowedHeaders: NonEmptyStringList,
+    additionalAllowedHeaders: NonEmptyStringList
 
     /// The names of headers that should be included in the
     /// Access-Control-Expose-Headers header in all responses sent by the
@@ -886,20 +1000,22 @@ structure cors {
     /// request headers bound to an output data member via the httpHeader,
     /// as well as any headers required by the protocol or authentication
     /// scheme.
-    additionalExposedHeaders: NonEmptyStringList,
+    additionalExposedHeaders: NonEmptyStringList
 }
 
 @private
 list NonEmptyStringList {
-    member: NonEmptyString,
+    member: NonEmptyString
 }
 
 /// Marks a member as the payload of an event.
 @trait(
-    selector: "structure > :test(member > :test(blob, string, structure, union))",
-    conflicts: [eventHeader],
-    structurallyExclusive: "member",
-    breakingChanges: [{change: "any"}]
+    selector: "structure > :test(member > :test(blob, string, structure, union))"
+    conflicts: [eventHeader]
+    structurallyExclusive: "member"
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 structure eventPayload {}
 
@@ -907,9 +1023,11 @@ structure eventPayload {}
 @trait(
     selector: """
         structure >
-        :test(member > :test(boolean, byte, short, integer, long, blob, string, timestamp))""",
-    conflicts: [eventPayload],
-    breakingChanges: [{change: "any"}]
+        :test(member > :test(boolean, byte, short, integer, long, blob, string, timestamp))"""
+    conflicts: [eventPayload]
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 structure eventHeader {}
 
@@ -922,25 +1040,26 @@ structure eventHeader {}
 @trait(selector: ":test(string, member > string)")
 structure idRef {
     /// Defines the selector that the resolved shape, if found, MUST match.
-    selector: String = "*",
+    selector: String = "*"
 
     /// When set to `true`, the shape ID MUST target a shape that can be
     /// found in the model.
-    failWhenMissing: Boolean,
+    failWhenMissing: Boolean
 
     /// Defines a custom error message to use when the shape ID cannot be
     /// found or does not match the selector.
     ///
     /// A default message is generated when errorMessage is not defined.
-    errorMessage: String,
+    errorMessage: String
 }
 
 @trait(
-    selector: ":test(timestamp, member > timestamp)",
-    breakingChanges: [{change: "any"}]
+    selector: ":test(timestamp, member > timestamp)"
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 enum timestampFormat {
-
     /// Date time as defined by the date-time production in RFC3339 section 5.6
     /// with no UTC offset (for example, 1985-04-12T23:20:50.52Z).
     DATE_TIME = "date-time"
@@ -957,8 +1076,10 @@ enum timestampFormat {
 
 /// Configures a custom operation endpoint.
 @trait(
-    selector: "operation",
-    breakingChanges: [{change: "any"}]
+    selector: "operation"
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 structure endpoint {
     /// A host prefix pattern for the operation.
@@ -966,14 +1087,16 @@ structure endpoint {
     /// Labels defined in the host pattern are used to bind top-level
     /// operation input members to the host.
     @required
-    hostPrefix: NonEmptyString,
+    hostPrefix: NonEmptyString
 }
 
 /// Binds a top-level operation input structure member to a label
 /// in the hostPrefix of an endpoint trait.
 @trait(
-    selector: "structure > :test(member[trait|required] > string)",
-    breakingChanges: [{change: "any"}]
+    selector: "structure > :test(member[trait|required] > string)"
+    breakingChanges: [
+        {change: "any"}
+    ]
 )
 structure hostLabel {}
 
@@ -993,17 +1116,21 @@ structure httpChecksumRequired {}
 
 /// Specializes a structure for use only as the input of a single operation.
 @trait(
-    selector: "structure",
-    conflicts: [output, error],
-    breakingChanges: [{change: "presence"}]
+    selector: "structure"
+    conflicts: [output, error]
+    breakingChanges: [
+        {change: "presence"}
+    ]
 )
 structure input {}
 
 /// Specializes a structure for use only as the output of a single operation.
 @trait(
-    selector: "structure",
-    conflicts: [input, error],
-    breakingChanges: [{change: "presence"}]
+    selector: "structure"
+    conflicts: [input, error]
+    breakingChanges: [
+        {change: "presence"}
+    ]
 )
 structure output {}
 
@@ -1025,16 +1152,17 @@ list LocalMixinTraitList {
 }
 
 @idRef(
-    selector: "[trait|trait]",
-    failWhenMissing: true,
+    selector: "[trait|trait]"
+    failWhenMissing: true
     errorMessage: """
-            Strings provided to the localTraits property of a mixin trait
-            must target a valid trait.""")
+        Strings provided to the localTraits property of a mixin trait
+        must target a valid trait."""
+)
 @private
 string LocalMixinTrait
 
-/// Defines the priority-ordered list of compression algorithms supported by the
-/// service operation.
+/// Defines the priority-ordered list of compression algorithms supported by
+/// the service operation.
 @private
 list RequestCompressionEncodingsList {
     // Encodings are NOT constrained in a Smithy enum since supported encodings
@@ -1051,8 +1179,8 @@ list RequestCompressionEncodingsList {
             change: "remove"
             severity: "DANGER"
             message: """
-            Trait was removed so newly generated clients will no longer compress requests, \
-            but the service MUST continue to support removed compression algorithms in `encodings`."""
+                Trait was removed so newly generated clients will no longer compress requests, \
+                but the service MUST continue to support removed compression algorithms in `encodings`."""
         }
         {
             change: "remove"
@@ -1064,26 +1192,26 @@ list RequestCompressionEncodingsList {
             severity: "NOTE"
             path: "/encodings/member"
             message: """
-            Members of `encodings` were added. Once a compression algorithm is added, \
-            the service MUST support the compression algorithm."""
+                Members of `encodings` were added. Once a compression algorithm is added, \
+                the service MUST support the compression algorithm."""
         }
         {
             change: "remove"
             severity: "DANGER"
             path: "/encodings/member"
             message: """
-            Members of `encodings` were removed so newly generated clients will no longer compress requests \
-            for removed compression algorithms. The service MUST continue to support old clients by supporting \
-            removed compression algorithms."""
+                Members of `encodings` were removed so newly generated clients will no longer compress requests \
+                for removed compression algorithms. The service MUST continue to support old clients by supporting \
+                removed compression algorithms."""
         }
         {
             change: "update"
             severity: "DANGER"
             path: "/encodings/member"
             message: """
-            Members of `encodings` were updated so newly generated clients will no longer compress requests \
-            for compression algorithms prior to the updates. The service MUST continue to support old clients by \
-            supporting compression algorithms prior to the updates."""
+                Members of `encodings` were updated so newly generated clients will no longer compress requests \
+                for compression algorithms prior to the updates. The service MUST continue to support old clients by \
+                supporting compression algorithms prior to the updates."""
         }
     ]
 )
diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiVersion.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiVersion.java
index 27d939084..46a1a4fde 100644
--- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiVersion.java
+++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiVersion.java
@@ -15,25 +15,32 @@
 
 package software.amazon.smithy.openapi;
 
+import java.util.Set;
 import software.amazon.smithy.jsonschema.JsonSchemaVersion;
+import software.amazon.smithy.utils.SetUtils;
 import software.amazon.smithy.utils.SmithyInternalApi;
 
 /**
  * OpenAPI versions supported by the converter.
  */
 public enum OpenApiVersion {
-
-    VERSION_3_0_2("3.0.2", false, JsonSchemaVersion.DRAFT07),
-    VERSION_3_1_0("3.1.0", true, JsonSchemaVersion.DRAFT2020_12);
+    VERSION_3_0_2("3.0.2", false, JsonSchemaVersion.DRAFT07,
+            SetUtils.of("propertyNames", "contentMediaType")),
+    VERSION_3_1_0("3.1.0", true, JsonSchemaVersion.DRAFT2020_12,
+            SetUtils.of("contentMediaType"));
 
     private final String version;
     private final boolean supportsContentEncodingKeyword;
     private final JsonSchemaVersion jsonSchemaVersion;
+    // See https://swagger.io/docs/specification/data-models/keywords/ for 3.0.2.
+    private final Set<String> unsupportedKeywords;
 
-    OpenApiVersion(String version, boolean supportsContentEncodingKeyword, JsonSchemaVersion jsonSchemaVersion) {
+    OpenApiVersion(String version, boolean supportsContentEncodingKeyword, JsonSchemaVersion jsonSchemaVersion,
+                   Set<String> unsupportedKeywords) {
         this.version = version;
         this.supportsContentEncodingKeyword = supportsContentEncodingKeyword;
         this.jsonSchemaVersion = jsonSchemaVersion;
+        this.unsupportedKeywords = unsupportedKeywords;
     }
 
     @Override
@@ -46,6 +53,11 @@ public enum OpenApiVersion {
         return supportsContentEncodingKeyword;
     }
 
+    @SmithyInternalApi
+    public Set<String> getUnsupportedKeywords() {
+        return unsupportedKeywords;
+    }
+
     JsonSchemaVersion getJsonSchemaVersion() {
         return jsonSchemaVersion;
     }
diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapper.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapper.java
index 4a407eef0..8fecc349e 100644
--- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapper.java
+++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapper.java
@@ -22,7 +22,6 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
 import software.amazon.smithy.jsonschema.JsonSchemaConfig;
 import software.amazon.smithy.jsonschema.JsonSchemaMapper;
 import software.amazon.smithy.jsonschema.Schema;
@@ -35,7 +34,6 @@ import software.amazon.smithy.model.traits.TimestampFormatTrait;
 import software.amazon.smithy.openapi.OpenApiConfig;
 import software.amazon.smithy.openapi.model.ExternalDocumentation;
 import software.amazon.smithy.utils.MapUtils;
-import software.amazon.smithy.utils.SetUtils;
 
 /**
  * Applies OpenAPI extensions to a {@link Schema} using configuration settings
@@ -46,11 +44,6 @@ import software.amazon.smithy.utils.SetUtils;
  */
 public final class OpenApiJsonSchemaMapper implements JsonSchemaMapper {
 
-    /** See https://swagger.io/docs/specification/data-models/keywords/. */
-    private static final Set<String> UNSUPPORTED_KEYWORD_DIRECTIVES = SetUtils.of(
-            "propertyNames",
-            "contentMediaType");
-
     @Override
     public Schema.Builder updateSchema(Shape shape, Schema.Builder builder, JsonSchemaConfig config) {
         getResolvedExternalDocs(shape, config)
@@ -93,7 +86,10 @@ public final class OpenApiJsonSchemaMapper implements JsonSchemaMapper {
         }
 
         // Remove unsupported JSON Schema keywords.
-        UNSUPPORTED_KEYWORD_DIRECTIVES.forEach(builder::disableProperty);
+        if (config instanceof OpenApiConfig) {
+            OpenApiConfig openApiConfig = (OpenApiConfig) config;
+            openApiConfig.getVersion().getUnsupportedKeywords().forEach(builder::disableProperty);
+        }
 
         return builder;
     }
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java
index 0439e23f0..1a5107cf4 100644
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java
+++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java
@@ -334,6 +334,9 @@ public final class IoUtils {
             Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
                 @Override
                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                    // Workaround for Windows systems that set some git packfiles to readonly
+                    file.toFile().setWritable(true);
+
                     Files.delete(file);
                     return FileVisitResult.CONTINUE;
                 }


Test patch:
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java
index 24c7525b6..29fb1da6e 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java
@@ -4,18 +4,25 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasLength;
 import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
+
 import org.junit.jupiter.api.Test;
+import software.amazon.smithy.utils.IoUtils;
 import software.amazon.smithy.utils.ListUtils;
 
+
 public class CleanCommandTest {
+    private static final String PROJECT_NAME = "simple-config-sources";
     @Test
     public void exitNormallyIfBuildDirMissing() {
-        IntegUtils.run("simple-config-sources", ListUtils.of("clean"), result -> {
+        IntegUtils.run(PROJECT_NAME, ListUtils.of("clean"), result -> {
             assertThat(result.getExitCode(), equalTo(0));
             assertThat(result.getOutput(), hasLength(0));
         });
@@ -23,7 +30,7 @@ public class CleanCommandTest {
 
     @Test
     public void deletesContentsOfBuildDir() {
-        IntegUtils.withProject("simple-config-sources", root -> {
+        IntegUtils.withProject(PROJECT_NAME, root -> {
             try {
                 Path created = Files.createDirectories(root.resolve("build").resolve("smithy").resolve("foo"));
                 assertThat(Files.exists(created), is(true));
@@ -35,4 +42,52 @@ public class CleanCommandTest {
             }
         });
     }
+
+    @Test
+    public void cleanRemovesAllCacheDirectories() throws IOException {
+        IntegUtils.clearCacheDirIfExists();
+        try {
+            Files.createDirectories(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH);
+            assertTrue(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH)
+                    && Files.isDirectory(IntegUtils.SMITHY_ROOT_CACHE_PATH));
+            IntegUtils.run(PROJECT_NAME, ListUtils.of("clean"), result -> {
+                assertThat(result.getExitCode(), equalTo(0));
+                assertThat(result.getOutput(), hasLength(0));
+                assertFalse(Files.exists(IntegUtils.SMITHY_ROOT_CACHE_PATH));
+            });
+        } finally {
+            IntegUtils.clearCacheDirIfExists();
+        }
+    }
+
+    @Test
+    public void cleanWithTemplateOptionRemovesOnlyTemplateDir() throws IOException {
+        IntegUtils.clearCacheDirIfExists();
+        try {
+            Files.createDirectories(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH);
+            assertTrue(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH)
+                    && Files.isDirectory(IntegUtils.SMITHY_ROOT_CACHE_PATH));
+
+            IntegUtils.withProject(PROJECT_NAME, root -> {
+                Path created = null;
+                try {
+                    created = Files.createDirectories(root.resolve("build").resolve("smithy").resolve("foo"));
+                    assertThat(Files.exists(created), is(true));
+                    RunResult result = IntegUtils.run(root, ListUtils.of("clean", "--templates"));
+                    assertThat(Files.exists(created), is(true));
+                    assertThat(result.getExitCode(), is(0));
+                    assertTrue(Files.exists(IntegUtils.SMITHY_ROOT_CACHE_PATH));
+                    assertFalse(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH));
+                } catch (IOException e) {
+                    throw new UncheckedIOException(e);
+                } finally {
+                    if (created != null) {
+                        IoUtils.rmdir(created);
+                    }
+                }
+            });
+        } finally {
+            IntegUtils.clearCacheDirIfExists();
+        }
+    }
 }
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java
index 2667a6606..102a97993 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java
@@ -3,7 +3,12 @@ package software.amazon.smithy.cli;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.emptyString;
+import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.utils.IoUtils;
 import software.amazon.smithy.utils.ListUtils;
@@ -121,15 +126,21 @@ public class InitCommandTest {
                         .append("Invalid template `blabla`. `Smithy-Examples` provides the following templates:")
                         .append(System.lineSeparator())
                         .append(System.lineSeparator())
-                        .append("NAME                    DOCUMENTATION")
+                        .append("─────────────────────  ─────────────────────────────────────────────────────────────────────────────")
+                        .append(System.lineSeparator())
+                        .append("NAME                   DOCUMENTATION")
+                        .append(System.lineSeparator())
+                        .append("─────────────────────  ─────────────────────────────────────────────────────────────────────────────")
+                        .append(System.lineSeparator())
+                        .append("bad-include-path       Template with incorrect include path. Should throw error.")
                         .append(System.lineSeparator())
-                        .append("---------------------   -----------------------------------------------------")
+                        .append("bad-template-path      Template with incorrect path. Should throw error.")
                         .append(System.lineSeparator())
-                        .append("included-file-json      Smithy Quickstart example with json file included.   ")
+                        .append("included-file-json     Smithy Quickstart example with json file included.")
                         .append(System.lineSeparator())
-                        .append("included-files-gradle   Smithy Quickstart example with gradle files included.")
+                        .append("included-files-gradle  Smithy Quickstart example with gradle files included.")
                         .append(System.lineSeparator())
-                        .append("quickstart-cli          Smithy Quickstart example weather service.           ")
+                        .append("quickstart-cli         Smithy Quickstart example weather service.")
                         .append(System.lineSeparator())
                         .toString();
 
@@ -191,17 +202,23 @@ public class InitCommandTest {
                     "init", "--list", "--url", templatesDir.toString()));
 
                 String expectedOutput = new StringBuilder()
-                    .append("NAME                    DOCUMENTATION")
-                    .append(System.lineSeparator())
-                    .append("---------------------   -----------------------------------------------------")
-                    .append(System.lineSeparator())
-                    .append("included-file-json      Smithy Quickstart example with json file included.   ")
-                    .append(System.lineSeparator())
-                    .append("included-files-gradle   Smithy Quickstart example with gradle files included.")
-                    .append(System.lineSeparator())
-                    .append("quickstart-cli          Smithy Quickstart example weather service.           ")
-                    .append(System.lineSeparator())
-                    .toString();
+                        .append("─────────────────────  ─────────────────────────────────────────────────────────────────────────────")
+                        .append(System.lineSeparator())
+                        .append("NAME                   DOCUMENTATION")
+                        .append(System.lineSeparator())
+                        .append("─────────────────────  ─────────────────────────────────────────────────────────────────────────────")
+                        .append(System.lineSeparator())
+                        .append("bad-include-path       Template with incorrect include path. Should throw error.")
+                        .append(System.lineSeparator())
+                        .append("bad-template-path      Template with incorrect path. Should throw error.")
+                        .append(System.lineSeparator())
+                        .append("included-file-json     Smithy Quickstart example with json file included.")
+                        .append(System.lineSeparator())
+                        .append("included-files-gradle  Smithy Quickstart example with gradle files included.")
+                        .append(System.lineSeparator())
+                        .append("quickstart-cli         Smithy Quickstart example weather service.")
+                        .append(System.lineSeparator())
+                        .toString();
 
                 assertThat(result.getOutput(), containsString(expectedOutput));
                 assertThat(result.getExitCode(), is(0));
@@ -249,6 +266,88 @@ public class InitCommandTest {
         });
     }
 
+    @Test
+    public void badTemplatePathFailureExpected() {
+        IntegUtils.withProject(PROJECT_NAME, templatesDir -> {
+            setupTemplatesDirectory(templatesDir);
+
+            IntegUtils.withTempDir("badTemplatePath", dir -> {
+                RunResult result = IntegUtils.run(
+                        dir, ListUtils.of("init", "-t", "bad-template-path", "-u", templatesDir.toString()));
+                assertThat(Files.exists(Paths.get(dir.toString(), "bad-template-path")), is(false));
+                assertThat(result.getExitCode(), is(1));
+                assertThat(result.getOutput(),
+                        containsString("Template path `getting-started-example/does-not-exist` for template"
+                                +" \"bad-template-path\" is invalid."));
+            });
+        });
+    }
+
+    @Test
+    public void badIncludePathFailureExpected() {
+        IntegUtils.withProject(PROJECT_NAME, templatesDir -> {
+            setupTemplatesDirectory(templatesDir);
+
+            IntegUtils.withTempDir("badIncludePath", dir -> {
+                RunResult result = IntegUtils.run(
+                        dir, ListUtils.of("init", "-t", "bad-include-path", "-u", templatesDir.toString()));
+                assertThat(Files.exists(Paths.get(dir.toString(), " bad-include-path")), is(false));
+                assertThat(result.getExitCode(), is(1));
+                assertThat(result.getOutput(),
+                        containsString("File or directory `getting-started-example/does-not-exist` is marked"
+                                + " for inclusion in template \"bad-include-path\", but was not found"));
+            });
+        });
+    }
+
+    @Test
+    public void cacheCreatedOnFirstCreationOfTemplate() {
+        IntegUtils.clearCacheDirIfExists();
+        IntegUtils.withProject(PROJECT_NAME, root -> {
+            try {
+                RunResult resultFirst = IntegUtils.run(root, ListUtils.of("init", "-o", "hello-world"));
+                assertTrue(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH)
+                        && Files.isDirectory(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH));
+                assertThat(resultFirst.getExitCode(), equalTo(0));
+                assertThat(resultFirst.getOutput(),
+                        containsString("template repo cloned"));
+                assertThat(resultFirst.getOutput(),
+                        containsString("Smithy project created in directory: hello-world"));
+                IoUtils.rmdir(root.resolve("hello-world"));
+
+                RunResult resultSecond = IntegUtils.run(root, ListUtils.of("init", "-o", "hello-world"));
+                assertTrue(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH)
+                        && Files.isDirectory(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH));
+                assertThat(resultSecond.getExitCode(), equalTo(0));
+                assertThat(resultSecond.getOutput(), not(containsString("template repo cloned")));
+                assertThat(resultSecond.getOutput(),
+                        containsString("Smithy project created in directory: hello-world"));
+            } finally {
+                IntegUtils.clearCacheDirIfExists();
+            }
+        });
+    }
+
+    @Test
+    public void noCacheCreatedWhenLocalRepo() {
+        IntegUtils.clearCacheDirIfExists();
+        IntegUtils.withProject(PROJECT_NAME, templatesDir -> {
+            setupTemplatesDirectory(templatesDir);
+
+            IntegUtils.withTempDir("exitZero", dir -> {
+                RunResult result = IntegUtils.run(
+                        dir, ListUtils.of("init", "-t", "quickstart-cli", "-u", templatesDir.toString()));
+                assertThat(result.getOutput(),
+                        containsString("Smithy project created in directory: quickstart-cli"));
+                assertThat(result.getExitCode(), is(0));
+                assertThat(Files.exists(Paths.get(dir.toString(), "quickstart-cli")), is(true));
+
+                assertFalse(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH));
+                assertFalse(Files.exists(IntegUtils.SMITHY_ROOT_CACHE_PATH));
+            });
+        });
+    }
+
     private static void run(List<String> args, Path root) {
         StringBuilder output = new StringBuilder();
         int result = IoUtils.runCommand(args, root, output, Collections.emptyMap());
diff --git a/smithy-diff/src/test/java/software/amazon/smithy/diff/ModelDiffTest.java b/smithy-diff/src/test/java/software/amazon/smithy/diff/ModelDiffTest.java
index ab6b72d0f..cbc22a416 100644
--- a/smithy-diff/src/test/java/software/amazon/smithy/diff/ModelDiffTest.java
+++ b/smithy-diff/src/test/java/software/amazon/smithy/diff/ModelDiffTest.java
@@ -94,4 +94,33 @@ public class ModelDiffTest {
 
         assertThat(result.isDiffBreaking(), is(false));
     }
+
+    @Test
+    public void appliesSuppressionsToDiff() {
+        Model oldModel = Model.assembler()
+                .addImport(getClass().getResource("suppressions-a.smithy"))
+                .assemble()
+                .unwrap();
+        Model newModel = Model.assembler()
+                .addImport(getClass().getResource("suppressions-b.smithy"))
+                .assemble()
+                .unwrap();
+
+        ModelDiff.Result result = ModelDiff.builder()
+                .oldModel(oldModel)
+                .newModel(newModel)
+                .compare();
+
+        assertThat(result.isDiffBreaking(), is(false));
+
+        boolean found = false;
+        for (ValidationEvent event : result.getDiffEvents()) {
+            if (event.getId().equals("ChangedMemberOrder")) {
+                assertThat(event.getSeverity(), equalTo(Severity.SUPPRESSED));
+                found = true;
+            }
+        }
+
+        assertThat(found, is(true));
+    }
 }
diff --git a/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-a.smithy b/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-a.smithy
new file mode 100644
index 000000000..4095cadf7
--- /dev/null
+++ b/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-a.smithy
@@ -0,0 +1,16 @@
+$version: "2.0"
+
+metadata suppressions = [
+    {
+        id: "ChangedMemberOrder"
+        namespace: "smithy.example"
+    }
+]
+
+namespace smithy.example
+
+structure Foo {
+    a: String
+    b: String
+    c: String
+}
diff --git a/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-b.smithy b/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-b.smithy
new file mode 100644
index 000000000..b08378e4e
--- /dev/null
+++ b/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-b.smithy
@@ -0,0 +1,16 @@
+$version: "2.0"
+
+metadata suppressions = [
+    {
+        id: "ChangedMemberOrder"
+        namespace: "smithy.example"
+    }
+]
+
+namespace smithy.example
+
+structure Foo {
+    c: String
+    a: String
+    b: String
+}
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecoratorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecoratorTest.java
new file mode 100644
index 000000000..21243e69c
--- /dev/null
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecoratorTest.java
@@ -0,0 +1,95 @@
+package software.amazon.smithy.model.validation.suppressions;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.not;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.validation.Severity;
+import software.amazon.smithy.model.validation.ValidatedResult;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidationEventDecorator;
+import software.amazon.smithy.model.validation.Validator;
+import software.amazon.smithy.model.validation.ValidatorFactory;
+
+public class ModelBasedEventDecoratorTest {
+    @Test
+    public void erroneousSuppressionsEmitEvents() {
+        ValidatedResult<Model> result = Model.assembler()
+                .addImport(getClass().getResource("bad-suppressions.smithy"))
+                // Ensure that events found while loading the model are decorated too.
+                .validatorFactory(testFactory(event -> event.toBuilder().hint("hi").build()))
+                .assemble();
+
+        assertThat(result.getValidationEvents(Severity.ERROR), not(empty()));
+        assertThat(result.getValidationEvents(Severity.ERROR).get(0).getMessage(), containsString("member `id`"));
+        assertThat(result.getValidationEvents(Severity.ERROR).get(0).getHint(), equalTo(Optional.of("hi")));
+    }
+
+    @Test
+    public void erroneousOverridesEmitEvents() {
+        ValidatedResult<Model> result = Model.assembler()
+                .addImport(getClass().getResource("bad-severityOverrides.smithy"))
+                // Ensure that events found while loading the model are decorated too.
+                .validatorFactory(testFactory(event -> event.toBuilder().hint("hi").build()))
+                .assemble();
+
+        assertThat(result.getValidationEvents(Severity.ERROR), not(empty()));
+        assertThat(result.getValidationEvents(Severity.ERROR).get(0).getMessage(), containsString("member `id`"));
+        assertThat(result.getValidationEvents(Severity.ERROR).get(0).getHint(), equalTo(Optional.of("hi")));
+    }
+
+    @Test
+    public void loadsSuppressionsAndOverrides() {
+        ValidatedResult<Model> result = Model.assembler()
+                .addImport(Model.class.getResource(
+                        "errorfiles/validators/severityOverrides/suppressions-take-precedence.smithy"))
+                .validatorFactory(testFactory(event -> event.toBuilder().hint("hi").build()))
+                .assemble();
+
+        assertThat(result.getValidationEvents(Severity.ERROR), empty());
+        assertThat(result.getValidationEvents(), not(empty()));
+
+        // Every event should have the applied hint.
+        for (ValidationEvent event : result.getValidationEvents()) {
+            assertThat(event.getHint(), equalTo(Optional.of("hi")));
+        }
+    }
+
+    private static ValidatorFactory testFactory(Function<ValidationEvent, ValidationEvent> d) {
+        return new ValidatorFactory() {
+            @Override
+            public List<Validator> loadBuiltinValidators() {
+                return Collections.emptyList();
+            }
+
+            @Override
+            public Optional<Validator> createValidator(String name, ObjectNode configuration) {
+                return Optional.empty();
+            }
+
+            @Override
+            public List<ValidationEventDecorator> loadDecorators() {
+                return Collections.singletonList(new ValidationEventDecorator() {
+                    @Override
+                    public boolean canDecorate(ValidationEvent ev) {
+                        return true;
+                    }
+
+                    @Override
+                    public ValidationEvent decorate(ValidationEvent ev) {
+                        return d.apply(ev);
+                    }
+                });
+            }
+        };
+    }
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-severityOverrides.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-severityOverrides.smithy
new file mode 100644
index 000000000..ba198649d
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-severityOverrides.smithy
@@ -0,0 +1,7 @@
+$version: "2.0"
+
+metadata severityOverrides = [
+    {
+        // missing id
+    }
+]
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-suppressions.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-suppressions.smithy
new file mode 100644
index 000000000..a942f4dab
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-suppressions.smithy
@@ -0,0 +1,7 @@
+$version: "2.0"
+
+metadata suppressions = [
+    {
+        // missing id
+    }
+]
diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverterTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverterTest.java
index 7c519fbc5..2c3fcdc80 100644
--- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverterTest.java
+++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverterTest.java
@@ -603,6 +603,7 @@ public class OpenApiConverterTest {
 
         Node.assertEquals(result, expectedNode);
     }
+
     @Test
     public void convertsToOpenAPI3_1_0() {
         Model model = Model.assembler()
diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapperTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapperTest.java
index b1603c4b6..ceeee93ed 100644
--- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapperTest.java
+++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapperTest.java
@@ -42,7 +42,6 @@ import software.amazon.smithy.model.shapes.MemberShape;
 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.traits.BoxTrait;
 import software.amazon.smithy.model.traits.DeprecatedTrait;
 import software.amazon.smithy.model.traits.ExternalDocumentationTrait;
 import software.amazon.smithy.model.traits.SensitiveTrait;
@@ -67,23 +66,6 @@ public class OpenApiJsonSchemaMapperTest {
         assertTrue(document.toNode().expectObjectNode().getMember("components").isPresent());
     }
 
-    @Test
-    public void stripsUnsupportedKeywords() {
-        StringShape string = StringShape.builder().id("smithy.api#String").build();
-        MemberShape key = MemberShape.builder().id("smithy.example#Map$key").target("smithy.api#String").build();
-        MemberShape value = MemberShape.builder().id("smithy.example#Map$value").target("smithy.api#String").build();
-        MapShape shape = MapShape.builder().id("smithy.example#Map").key(key).value(value).build();
-        Model model = Model.builder().addShapes(string, shape, key, value).build();
-        SchemaDocument document = JsonSchemaConverter.builder()
-                .addMapper(new OpenApiJsonSchemaMapper())
-                .model(model)
-                .build()
-                .convertShape(shape);
-        Schema schema = document.getRootSchema();
-
-        assertFalse(schema.getPropertyNames().isPresent());
-    }
-
     @Test
     public void supportsExternalDocs() {
         String key = "Homepage";
diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/nullability-and-format.smithy b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/nullability-and-format.smithy
index b2f84b51a..32bc3581a 100644
--- a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/nullability-and-format.smithy
+++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/nullability-and-format.smithy
@@ -26,7 +26,8 @@ blob FilePayload
 
 @output
 structure FooBarOutput {
-    bar: BoxedInteger
+    bar: BoxedInteger,
+    baz: MyMap
 }
 
 @error("client")
@@ -36,3 +37,18 @@ structure FooBarError {
 
 @box
 integer BoxedInteger
+
+map MyMap {
+    key: MyEnum,
+    value: String
+}
+
+@enum([
+    {
+        value: "FOO"
+    },
+    {
+        value: "BAR"
+    }
+])
+string MyEnum
diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/openapi-3-0-2.openapi.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/openapi-3-0-2.openapi.json
index 1293e4ffc..9425372b3 100644
--- a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/openapi-3-0-2.openapi.json
+++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/openapi-3-0-2.openapi.json
@@ -69,8 +69,17 @@
         "properties": {
           "bar": {
             "type": "number"
+          },
+          "baz": {
+            "$ref": "#/components/schemas/MyMap"
           }
         }
+      },
+      "MyMap": {
+        "type": "object",
+        "additionalProperties": {
+          "type": "string"
+        }
       }
     }
   }
diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/openapi-3-1-0.openapi.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/openapi-3-1-0.openapi.json
index 0c6c3b6b3..77c681959 100644
--- a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/openapi-3-1-0.openapi.json
+++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/openapi-3-1-0.openapi.json
@@ -69,8 +69,27 @@
         "properties": {
           "bar": {
             "type": "number"
+          },
+          "baz": {
+            "$ref": "#/components/schemas/MyMap"
           }
         }
+      },
+      "MyEnum": {
+        "type": "string",
+        "enum": [
+          "FOO",
+          "BAR"
+        ]
+      },
+      "MyMap": {
+        "type": "object",
+        "propertyNames": {
+          "$ref": "#/components/schemas/MyEnum"
+        },
+        "additionalProperties": {
+          "type": "string"
+        }
       }
     }
   }