Repository: smithy-lang/smithy
Pull request #2125 (2024-02-05 18:48:28)

Add JSON protocol tests for document-valued maps
*Issue #, if available:*

(none)

*Description of changes:*

This change adds tests for serializing/deserializing maps which use the document shape as a value. The AWS SDK for Kotlin previously did not handle these correctly, leading to a customer contact.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/docs/source-2.0/guides/model-validation-examples.rst b/docs/source-2.0/guides/model-validation-examples.rst
index 3250c1d2a..8e1f2c553 100644
--- a/docs/source-2.0/guides/model-validation-examples.rst
+++ b/docs/source-2.0/guides/model-validation-examples.rst
@@ -314,7 +314,7 @@ linters to have clear, actionable error messages.
 Require strings to have a ``@pattern`` constraint
 =================================================
 
-This example shows how to require all integers used in an operation input to
+This example shows how to require all strings used in an operation input to
 have a ``@pattern`` constraint trait.
 
 .. code-block:: smithy
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/AstCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/AstCommand.java
index c89af9717..25e6fab95 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/AstCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/AstCommand.java
@@ -86,7 +86,7 @@ final class AstCommand implements Command {
                 .models(arguments.getPositional())
                 .validationPrinter(env.stderr())
                 .validationMode(Validator.Mode.QUIET)
-                .severity(Severity.DANGER)
+                .defaultSeverity(Severity.DANGER)
                 .build();
 
         ModelSerializer serializer = ModelSerializer.builder().build();
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java
index 25508cdfd..0ed1e38b0 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java
@@ -72,7 +72,7 @@ final class BuildCommand implements Command {
     public int execute(Arguments arguments, Env env) {
         arguments.addReceiver(new ConfigOptions());
         arguments.addReceiver(new DiscoveryOptions());
-        arguments.addReceiver(new SeverityOption());
+        arguments.addReceiver(new ValidatorOptions());
         arguments.addReceiver(new BuildOptions());
         arguments.addReceiver(new Options());
 
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java
index c0a7c10f3..e6567e1ae 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java
@@ -65,7 +65,7 @@ final class DiffCommand implements Command {
     @Override
     public int execute(Arguments arguments, Env env) {
         arguments.addReceiver(new ConfigOptions());
-        arguments.addReceiver(new SeverityOption());
+        arguments.addReceiver(new ValidatorOptions());
         arguments.addReceiver(new BuildOptions());
         arguments.addReceiver(new Options());
         arguments.getReceiver(BuildOptions.class).noPositionalArguments(true);
@@ -317,7 +317,7 @@ final class DiffCommand implements Command {
                     .validationPrinter(env.stderr())
                     // Only report issues that fail the build.
                     .validationMode(Validator.Mode.QUIET_CORE_ONLY)
-                    .severity(Severity.DANGER);
+                    .defaultSeverity(Severity.DANGER);
         }
 
         // Creating a new model is the same for each diff mode.
@@ -336,7 +336,7 @@ final class DiffCommand implements Command {
             builder
                     .titleLabel("DIFF", ColorTheme.DIFF_TITLE)
                     .validatedResult(new ValidatedResult<>(newModel, events))
-                    .severity(null) // reset so it takes on standard option settings.
+                    .defaultSeverity(null) // reset so it takes on standard option settings.
                     .build();
         }
 
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/MigrateCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/MigrateCommand.java
index e081e6c0d..92b70dedc 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/MigrateCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/MigrateCommand.java
@@ -152,7 +152,7 @@ final class MigrateCommand implements Command {
                 .env(env)
                 .models(models)
                 .validationPrinter(env.stderr())
-                .severity(Severity.DANGER)
+                .defaultSeverity(Severity.DANGER)
                 .build();
 
         SmithyBuild smithyBuild = SmithyBuild.create(classLoader)
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java
index b9020bee8..de67ab126 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java
@@ -57,7 +57,7 @@ final class ModelBuilder {
     private List<String> models;
     private Command.Env env;
     private SmithyBuildConfig config;
-    private Severity severity;
+    private Severity defaultSeverity;
     private ValidatedResult<Model> validatedResult;
     private String titleLabel;
     private Style[] titleLabelStyles;
@@ -94,8 +94,8 @@ final class ModelBuilder {
         return this;
     }
 
-    public ModelBuilder severity(Severity severity) {
-        this.severity = severity;
+    public ModelBuilder defaultSeverity(Severity defaultSeverity) {
+        this.defaultSeverity = defaultSeverity;
         return this;
     }
 
@@ -123,7 +123,13 @@ final class ModelBuilder {
 
         StandardOptions standardOptions = arguments.getReceiver(StandardOptions.class);
         BuildOptions buildOptions = arguments.getReceiver(BuildOptions.class);
-        Severity minSeverity = resolveMinSeverity(standardOptions);
+
+        // Resolve validator options and severity.
+        ValidatorOptions validatorOptions = arguments.hasReceiver(ValidatorOptions.class)
+                ? arguments.getReceiver(ValidatorOptions.class)
+                : new ValidatorOptions();
+        resolveMinSeverity(standardOptions, validatorOptions);
+
         ClassLoader classLoader = env.classLoader();
         ColorFormatter colors = env.colors();
         CliPrinter stderr = env.stderr();
@@ -176,8 +182,8 @@ final class ModelBuilder {
 
         for (ValidationEvent event : sortedEvents) {
             // Only log events that are >= --severity. Note that setting --quiet inherently
-            // configures events to need to be >= DANGER.
-            if (event.getSeverity().ordinal() >= minSeverity.ordinal()) {
+            // configures events to need to be >= DANGER. Also filter using --show-validators and --hide-validators.
+            if (validatorOptions.isVisible(event)) {
                 validationPrinter.println(formatter.format(event));
             }
         }
@@ -274,14 +280,11 @@ final class ModelBuilder {
     }
 
     // Determine a default severity if one wasn't given, by inspecting if there is a --severity option.
-    private Severity resolveMinSeverity(StandardOptions standardOptions) {
-        if (severity != null) {
-            return severity;
-        } else if (arguments.hasReceiver(SeverityOption.class)) {
-            return arguments.getReceiver(SeverityOption.class).severity(standardOptions);
-        } else {
-            return Severity.WARNING;
+    private Severity resolveMinSeverity(StandardOptions standardOptions, ValidatorOptions validatorOption) {
+        if (defaultSeverity != null && validatorOption.severity() == null) {
+            validatorOption.severity(defaultSeverity);
         }
+        return validatorOption.detectAndGetSeverity(standardOptions);
     }
 
     static ModelAssembler createModelAssembler(ClassLoader classLoader) {
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java
index 3f2548cb9..b0de900fb 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java
@@ -232,7 +232,7 @@ final class SelectCommand implements Command {
                 .models(arguments.getPositional())
                 .validationPrinter(env.stderr())
                 .validationMode(Validator.Mode.QUIET_CORE_ONLY)
-                .severity(Severity.DANGER)
+                .defaultSeverity(Severity.DANGER)
                 .build();
 
         Options options = arguments.getReceiver(Options.class);
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SeverityOption.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SeverityOption.java
deleted file mode 100644
index febca698a..000000000
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SeverityOption.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.cli.commands;
-
-import java.util.function.Consumer;
-import software.amazon.smithy.cli.ArgumentReceiver;
-import software.amazon.smithy.cli.CliError;
-import software.amazon.smithy.cli.HelpPrinter;
-import software.amazon.smithy.cli.StandardOptions;
-import software.amazon.smithy.model.validation.Severity;
-
-/**
- * Add the --severity option.
- */
-final class SeverityOption implements ArgumentReceiver {
-
-    static final String SEVERITY = "--severity";
-
-    private Severity severity;
-
-    @Override
-    public void registerHelp(HelpPrinter printer) {
-        printer.param(SEVERITY, null, "SEVERITY", "Set the minimum reported validation severity (one of NOTE, "
-                                                  + "WARNING [default setting], DANGER, ERROR).");
-    }
-
-    @Override
-    public Consumer<String> testParameter(String name) {
-        if (SEVERITY.equals(name)) {
-            return value -> {
-                severity(Severity.fromString(value).orElseThrow(() -> {
-                    return new CliError("Invalid severity level: " + value);
-                }));
-            };
-        }
-        return null;
-    }
-
-    /**
-     * Set the severity.
-     *
-     * @param severity Severity to set.
-     */
-    void severity(Severity severity) {
-        this.severity = severity;
-    }
-
-    /**
-     * Get the severity level, taking into account standard options that affect the default.
-     *
-     * @param options Standard options to query if no severity is explicitly set.
-     * @return Returns the resolved severity option.
-     */
-    Severity severity(StandardOptions options) {
-        if (severity != null) {
-            return severity;
-        } else if (options.quiet()) {
-            return Severity.DANGER;
-        } else {
-            return Severity.WARNING;
-        }
-    }
-}
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidateCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidateCommand.java
index 4cc3f2960..34c7ea5ea 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidateCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidateCommand.java
@@ -46,7 +46,7 @@ final class ValidateCommand implements Command {
     public int execute(Arguments arguments, Env env) {
         arguments.addReceiver(new ConfigOptions());
         arguments.addReceiver(new DiscoveryOptions());
-        arguments.addReceiver(new SeverityOption());
+        arguments.addReceiver(new ValidatorOptions());
         arguments.addReceiver(new BuildOptions());
 
         CommandAction action = HelpActionWrapper.fromCommand(
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidatorOptions.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidatorOptions.java
new file mode 100644
index 000000000..98120f9c7
--- /dev/null
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidatorOptions.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.cli.commands;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import software.amazon.smithy.cli.ArgumentReceiver;
+import software.amazon.smithy.cli.CliError;
+import software.amazon.smithy.cli.HelpPrinter;
+import software.amazon.smithy.cli.StandardOptions;
+import software.amazon.smithy.model.validation.Severity;
+import software.amazon.smithy.model.validation.ValidationEvent;
+
+/**
+ * Add validation specific options.
+ */
+final class ValidatorOptions implements ArgumentReceiver {
+
+    static final String SEVERITY = "--severity";
+    static final String SHOW_VALIDATORS = "--show-validators";
+    static final String HIDE_VALIDATORS = "--hide-validators";
+
+    private Severity severity;
+    private List<String> showValidators = Collections.emptyList();
+    private List<String> hideValidators = Collections.emptyList();
+
+    @Override
+    public void registerHelp(HelpPrinter printer) {
+        printer.param(SEVERITY, null, "SEVERITY", "Set the minimum reported validation severity (one of NOTE, "
+                                                  + "WARNING [default setting], DANGER, ERROR).");
+        printer.param(SHOW_VALIDATORS, null, "VALIDATORS", "Comma-separated list of hierarchical validation event "
+                                                           + "IDs to show in the output of the command, "
+                                                           + "hiding the rest.");
+        printer.param(HIDE_VALIDATORS, null, "VALIDATORS", "Comma-separated list of hierarchical validation event "
+                                                           + "IDs to hide in the output of the command, "
+                                                           + "showing the rest.");
+    }
+
+    @Override
+    public Consumer<String> testParameter(String name) {
+        switch (name) {
+            case SEVERITY:
+                return value -> {
+                    severity(Severity.fromString(value).orElseThrow(() -> {
+                        return new CliError("Invalid severity level: " + value);
+                    }));
+                };
+            case SHOW_VALIDATORS:
+                return value -> {
+                    if (!hideValidators.isEmpty()) {
+                        throw new CliError(SHOW_VALIDATORS + " and " + HIDE_VALIDATORS + " are mutually exclusive");
+                    }
+                    showValidators(parseIds(value));
+                };
+            case HIDE_VALIDATORS:
+                return value -> {
+                    if (!showValidators.isEmpty()) {
+                        throw new CliError(SHOW_VALIDATORS + " and " + HIDE_VALIDATORS + " are mutually exclusive");
+                    }
+                    hideValidators(parseIds(value));
+                };
+            default:
+                return null;
+        }
+    }
+
+    private List<String> parseIds(String value) {
+        List<String> result = new ArrayList<>();
+        for (String id : value.split("\\s*,\\s*")) {
+            id = id.trim();
+            if (id.isEmpty()) {
+                throw new CliError("Invalid validation event ID");
+            }
+            result.add(id);
+        }
+        return result;
+    }
+
+    /**
+     * Get the set severity, which may be null.
+     *
+     * @return The nullable severity.
+     */
+    Severity severity() {
+        return severity;
+    }
+
+    /**
+     * Set the severity.
+     *
+     * @param severity Severity to set.
+     */
+    void severity(Severity severity) {
+        this.severity = severity;
+    }
+
+    /**
+     * Set and get the severity level, taking into account standard options that affect the default.
+     *
+     * @param standardOptions  Standard options to query if no severity is explicitly set.
+     * @return Returns the resolved severity option.
+     */
+    Severity detectAndGetSeverity(StandardOptions standardOptions) {
+        if (severity == null) {
+            if (standardOptions.quiet()) {
+                severity = Severity.DANGER;
+            } else {
+                severity = Severity.WARNING;
+            }
+        }
+        return severity;
+    }
+
+    /**
+     * Set the list of validators to show, but hide everything else.
+     *
+     * @param validators Validators to show.
+     */
+    void showValidators(List<String> validators) {
+        this.showValidators = validators;
+    }
+
+    /**
+     * Get the list of validators to show.
+     *
+     * @return Validator event IDs.
+     */
+    List<String> showValidators() {
+        return showValidators;
+    }
+
+    /**
+     * Get the list of validators to hide.
+     *
+     * @return Validator event IDs.
+     */
+    List<String> hideValidators(String csv) {
+        return hideValidators;
+    }
+
+    /**
+     * Set the list of validators to hide.
+     *
+     * @param validators Validators to hide.
+     */
+    void hideValidators(List<String> validators) {
+        this.hideValidators = validators;
+    }
+
+    /**
+     * Check if the given validation event matches the show/hide settings.
+     *
+     * <p>A severity must be set before calling this method.
+     *
+     * @param event Event to check.
+     * @return Return true if the event can be seen.
+     */
+    boolean isVisible(ValidationEvent event) {
+        if (event.getSeverity().ordinal() < severity.ordinal()) {
+            return false;
+        }
+
+        if (!showValidators.isEmpty()) {
+            for (String show : showValidators) {
+                if (event.containsId(show)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        if (!hideValidators.isEmpty()) {
+            for (String hide : hideValidators) {
+                if (event.containsId(hide)) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+}
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
index a90ce944c..889451ff6 100644
--- 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
@@ -151,6 +151,11 @@ public final class ModelBasedEventDecorator {
             List<Suppression> suppressions,
             List<SeverityOverride> severityOverrides
     ) {
+        // ERROR and SUPPRESSED events cannot be suppressed.
+        if (!event.getSeverity().canSuppress()) {
+            return event;
+        }
+
         // Use a suppress trait if present.
         if (event.getShapeId().isPresent()) {
             ShapeId target = event.getShapeId().get();


Test patch:
diff --git a/smithy-aws-protocol-tests/model/restJson1/documents.smithy b/smithy-aws-protocol-tests/model/restJson1/documents.smithy
index 0d8b3318f..9ca1e68fe 100644
--- a/smithy-aws-protocol-tests/model/restJson1/documents.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/documents.smithy
@@ -324,3 +324,73 @@ apply DocumentTypeAsPayload @httpResponseTests([
         }
     }
 ])
+
+/// This example serializes documents as the value of maps.
+@idempotent
+@http(uri: "/DocumentTypeAsMapValue", method: "PUT")
+operation DocumentTypeAsMapValue {
+    input: DocumentTypeAsMapValueInputOutput,
+    output: DocumentTypeAsMapValueInputOutput,
+}
+
+structure DocumentTypeAsMapValueInputOutput {
+    docValuedMap: DocumentValuedMap,
+}
+
+map DocumentValuedMap {
+    key: String,
+    value: Document,
+}
+
+apply DocumentTypeAsMapValue @httpRequestTests([
+    {
+        id: "DocumentTypeAsMapValueInput",
+        documentation: "Serializes a map that uses documents as the value.",
+        protocol: restJson1,
+        method: "PUT",
+        uri: "/DocumentTypeAsMapValue",
+        body: """
+            {
+                "docValuedMap": {
+                    "foo": { "f": 1, "o": 2 },
+                    "bar": [ "b", "a", "r" ],
+                    "baz": "BAZ"
+                }
+            }""",
+        bodyMediaType: "application/json",
+        headers: {"Content-Type": "application/json"},
+        params: {
+            docValuedMap: {
+                "foo": { "f": 1, "o": 2 },
+                "bar": [ "b", "a", "r" ],
+                "baz": "BAZ",
+            },
+        },
+    },
+])
+
+apply DocumentTypeAsMapValue @httpResponseTests([
+    {
+        id: "DocumentTypeAsMapValueOutput",
+        documentation: "Serializes a map that uses documents as the value.",
+        protocol: restJson1,
+        code: 200,
+        body: """
+            {
+                "docValuedMap": {
+                    "foo": { "f": 1, "o": 2 },
+                    "bar": [ "b", "a", "r" ],
+                    "baz": "BAZ"
+                }
+            }""",
+        bodyMediaType: "application/json",
+        headers: {"Content-Type": "application/json"},
+        params: {
+            docValuedMap: {
+                "foo": { "f": 1, "o": 2 },
+                "bar": [ "b", "a", "r" ],
+                "baz": "BAZ",
+            },
+        },
+    },
+])
diff --git a/smithy-aws-protocol-tests/model/restJson1/http-query.smithy b/smithy-aws-protocol-tests/model/restJson1/http-query.smithy
index 995b18904..53a45a24c 100644
--- a/smithy-aws-protocol-tests/model/restJson1/http-query.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/http-query.smithy
@@ -232,6 +232,10 @@ apply AllQueryStringTypes @httpRequestTests([
         params: {
             queryInteger: 0
             queryBoolean: false
+            queryParamsMapOfStringList: {
+                queryInteger: ["0"]
+                queryBoolean: ["false"]
+            }
         }
     }
 ])
diff --git a/smithy-aws-protocol-tests/model/restJson1/main.smithy b/smithy-aws-protocol-tests/model/restJson1/main.smithy
index 16c779b4f..f0a638594 100644
--- a/smithy-aws-protocol-tests/model/restJson1/main.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/main.smithy
@@ -87,6 +87,7 @@ service RestJson {
         // Documents
         DocumentType,
         DocumentTypeAsPayload,
+        DocumentTypeAsMapValue,
 
         // Unions
         JsonUnions,
diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ValidateCommandTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ValidateCommandTest.java
index b3f2fcf32..942ead467 100644
--- a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ValidateCommandTest.java
+++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ValidateCommandTest.java
@@ -18,6 +18,7 @@ package software.amazon.smithy.cli.commands;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
 
 import java.net.URISyntaxException;
@@ -151,4 +152,68 @@ public class ValidateCommandTest {
         assertThat(result.code(), not(0));
         assertThat(result.stderr(), containsString("Invalid severity"));
     }
+
+    @Test
+    public void showAndHideAreExclusive() {
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--show-validators", "X", "--hide-validators", "Y");
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stderr(), containsString("--show-validators and --hide-validators are mutually exclusive"));
+    }
+
+    @Test
+    public void hideAndShowAreExclusive() {
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--hide-validators", "Y", "--show-validators", "X");
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stderr(), containsString("--show-validators and --hide-validators are mutually exclusive"));
+    }
+
+    @Test
+    public void validatesEventIdsAreNotEmpty() {
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--show-validators", "Foo,,Bar");
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stderr(), containsString("Invalid validation event ID"));
+    }
+
+    @Test
+    public void validatesEventIdsAreNotBlank() {
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--show-validators", "Foo, ,Bar");
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stderr(), containsString("Invalid validation event ID"));
+    }
+
+    @Test
+    public void validatesEventIdsAreNotLeadingEmpty() {
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--show-validators", ",Bar");
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stderr(), containsString("Invalid validation event ID"));
+    }
+
+    @Test
+    public void canShowEventsById() throws Exception {
+        Path validationEventsModel = Paths.get(getClass().getResource("validation-events.smithy").toURI());
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--debug", "--show-validators", "EmitWarnings",
+                                                    validationEventsModel.toString());
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stdout(), containsString("EmitWarnings"));
+        assertThat(result.stdout(), not(containsString("EmitDangers")));
+        assertThat(result.stdout(), not(containsString("HttpLabelTrait")));
+    }
+
+    @Test
+    public void canHideEventsById() throws Exception {
+        Path validationEventsModel = Paths.get(getClass().getResource("validation-events.smithy").toURI());
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--debug", "--hide-validators", "EmitDangers",
+                                                    validationEventsModel.toString());
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stdout(), containsString("EmitWarnings"));
+        assertThat(result.stdout(), not(containsString("EmitDangers")));
+        assertThat(result.stdout(), containsString("HttpLabelTrait"));
+    }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/PropertyBindingIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/PropertyBindingIndexTest.java
index b51c20ac8..c707142bc 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/PropertyBindingIndexTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/PropertyBindingIndexTest.java
@@ -15,20 +15,18 @@
 
 package software.amazon.smithy.model.knowledge;
 
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.hasSize;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.shapes.MemberShape;
 import software.amazon.smithy.model.shapes.OperationShape;
 import software.amazon.smithy.model.shapes.ShapeId;
-import software.amazon.smithy.model.validation.Severity;
 import software.amazon.smithy.model.validation.ValidatedResult;
+import software.amazon.smithy.model.validation.ValidatedResultException;
 
 public class PropertyBindingIndexTest {
 
@@ -71,8 +69,6 @@ public class PropertyBindingIndexTest {
         assertTrue(index.doesMemberShapeRequireProperty(model.expectShape(
                 ShapeId.from("com.example#ChangeResourceOutput$id"), MemberShape.class)));
 
-        assertThat(vrmodel.getValidationEvents(Severity.SUPPRESSED), hasSize(1));
-        assertThat(vrmodel.getValidationEvents(Severity.SUPPRESSED).get(0).getId(),
-                   equalTo("ResourceOperationInputOutput"));
+        Assertions.assertThrows(ValidatedResultException.class, () -> vrmodel.unwrap());
     }
 }
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/suppression-metadata-cannot-suppress-errors.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/suppression-metadata-cannot-suppress-errors.errors
new file mode 100644
index 000000000..2065c6e95
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/suppression-metadata-cannot-suppress-errors.errors
@@ -0,0 +1,1 @@
+[ERROR] foo.baz#MyString: Trait `idempotent` | TraitTarget
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/suppression-metadata-cannot-suppress-errors.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/suppression-metadata-cannot-suppress-errors.smithy
new file mode 100644
index 000000000..8bc35a091
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/suppression-metadata-cannot-suppress-errors.smithy
@@ -0,0 +1,13 @@
+$version: "2"
+
+metadata suppressions = [
+    {
+        namespace: "foo.baz"
+        id: "TraitTarget"
+    }
+]
+
+namespace foo.baz
+
+@idempotent // < this is invalid
+string MyString
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/suppression-trait-cannot-suppress-errors.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/suppression-trait-cannot-suppress-errors.errors
new file mode 100644
index 000000000..2065c6e95
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/suppression-trait-cannot-suppress-errors.errors
@@ -0,0 +1,1 @@
+[ERROR] foo.baz#MyString: Trait `idempotent` | TraitTarget
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/suppression-trait-cannot-suppress-errors.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/suppression-trait-cannot-suppress-errors.smithy
new file mode 100644
index 000000000..195f18366
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/suppression-trait-cannot-suppress-errors.smithy
@@ -0,0 +1,7 @@
+$version: "2"
+
+namespace foo.baz
+
+@suppress(["TraitTarget"])
+@idempotent // < this is invalid
+string MyString
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/member-property-index.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/member-property-index.smithy
index 572876364..2c9860a95 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/member-property-index.smithy
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/member-property-index.smithy
@@ -1,12 +1,5 @@
 $version: "2.0"
 
-metadata suppressions = [
-    {
-        id: "ResourceOperationInputOutput",
-        namespace: "com.example"
-    }
-]
-
 namespace com.example
 
 resource Resource1 {