Repository: smithy-lang/smithy
Pull request #2402 (2024-09-23 16:34:15)

Allow adding member docs to $ref
Addresses https://github.com/smithy-lang/smithy/issues/2400.

When a member targets a structure, it becomes a schema reference when converted to JSON Schema. Previously, we didn't add member docs to the converted object, possibly because earlier versions of open api or JSON Schema did not support it. Reading through [this issue](https://github.com/OAI/OpenAPI-Specification/issues/1514) the [OAI spec](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#reference-object), and the [JSON Schema Spec](https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3), it seems that OpenAPI 3.1 and JSON Schema 2020-12 support the `description` property alongside `$ref`. I wasn't able to find anything about whether it is supported in [JSON Schema 07](https://json-schema.org/draft-07/json-schema-release-notes). This commit adds a new config option, `addReferenceDescriptions` that will add the `description` property alongside `$ref` when the member has documentation. I made it opt-in through the config option so we don't cause any existing documentation to be changed unexpectedly.

---
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/aws/aws-core.rst b/docs/source-2.0/aws/aws-core.rst
index 44b5ea644..fb961d890 100644
--- a/docs/source-2.0/aws/aws-core.rst
+++ b/docs/source-2.0/aws/aws-core.rst
@@ -932,23 +932,21 @@ The ``httpChecksum`` trait is a structure that contains the following members:
     * - requestAlgorithmMember
       - ``string``
       - Defines a top-level operation input member that is used to configure
-        request checksum behavior. The input member MUST target a string shape
-        marked with the :ref:`enum-trait`. Each value in the enum represents a
-        supported checksum algorithm. Algorithms MUST be one of the following
-        supported values: "CRC64NVME", "CRC32C", "CRC32", "SHA1", or "SHA256".
+        request checksum behavior. The input member MUST target an :ref:`enum`
+        shape. Each value in the enum represents a supported checksum
+        algorithm. Algorithms MUST be one of the following supported values:
+        "CRC64NVME", "CRC32C", "CRC32", "SHA1", or "SHA256".
     * - requestChecksumRequired
       - ``boolean``
-      - Indicates an operation requires a checksum in its HTTP request. By
-        default, the checksum used for a service is an MD5 checksum passed
-        in the Content-MD5 header. When the input member represented by the
-        ``requestAlgorithmMember`` property is set, the default behavior is
-        disabled.
+      - Indicates an operation requires a checksum in its HTTP request. If the
+        input member represented by``requestAlgorithmMember`` property is not
+        set, the client uses its default algorithm.
     * - requestValidationModeMember
       - ``string``
-      - Defines a top-level operation input member used to opt-in to
-        best-effort validation of a checksum returned in the HTTP response of
-        the operation. The input member MUST target a string shape marked with
-        the :ref:`enum-trait` that contains the value "ENABLED".
+      - Defines a top-level operation input member used to opt-in to validation
+        of a checksum returned in the HTTP response of the operation. The input
+        member MUST target an :ref:`enum` shape that contains the value
+        "ENABLED".
     * - responseAlgorithms
       - ``set<string>``
       - Defines the checksum algorithms clients SHOULD look for when validating
@@ -1027,18 +1025,33 @@ behavior, will fail validation.
 Client behavior
 ===============
 
+Supported checksum algorithms
+-----------------------------
+The following checksum algorithms MUST be supported by clients.
+    * CRC32
+    * SHA1
+    * SHA256
+
+Additionally, the following checksum algorithms SHOULD be supported by clients.
+    * CRC64NVME
+    * CRC32C
+
 HTTP request checksums
 ----------------------
 
-When a client calls an operation which has the ``httpChecksum`` trait where
-``requestChecksumRequired`` is set to ``true``, the client MUST include a
-checksum in the HTTP request.
+By default, when a client calls an operation which has the ``httpChecksum``
+trait, the client MUST include a checksum in the HTTP request, unless the
+client is configured to only include checksum when ``requestChecksumRequired``
+is set to ``true``.
 
 When a client calls an operation which has the ``httpChecksum`` trait where
 ``requestAlgorithmMember`` is set, the client MUST look up the input member
 represented by ``requestAlgorithmMember`` property. The value of this member is
 the checksum algorithm that the client MUST use to compute the request payload
-checksum.
+checksum. If the client does not support that algorithm, it MUST fail the
+request. If the user did not set a value for the input member represented by
+``requestAlgorithmMember``, the client MUST use its default algorithm and
+implicitly set that value in ``requestAlgorithmMember``.
 
 The computed checksum MUST be supplied at a resolved location as per the
 :ref:`resolving checksum location <aws.protocols#httpChecksum-trait_resolving-checksum-location>`
@@ -1049,29 +1062,35 @@ of the body. The header or trailer name to use with an algorithm is resolved as
 per the :ref:`resolving checksum name <aws.protocols#httpChecksum-trait_resolving-checksum-name>`
 section.
 
-If no ``requestAlgorithmMember`` property is set, the client MUST compute an
-MD5 checksum of the request payload and place it in the ``Content-MD5`` header.
+If no ``requestAlgorithmMember`` property is set in the trait and
+``requestChecksumRequired`` is set to ``true``, the client MUST compute an MD5
+checksum of the request payload and place it in the ``Content-MD5`` header.
 
 If the HTTP header corresponding to a checksum is set explicitly, the client
 MUST use the explicitly set header and skip computing the payload checksum.
 
-.. seealso:: See :ref:`client behavior<aws.protocols#httpChecksum-trait_header-conflict-behavior>`
-    for more details.
-
 HTTP response checksums
 -----------------------
 
+When a client calls an operation which has the ``httpChecksum`` trait, the
+client MUST look up the input member represented by the
+``requestValidationModeMember`` property. If the user did not set a value for
+this member, by default the client MUST implicitly set it to ``ENABLED``,
+unless the client is configured to opt-out of this default behavior.
+
 When a client receives a response for an operation which has the ``httpChecksum``
 trait where the ``requestValidationModeMember`` property is set, the client
 MUST look up the input member represented by the property's value. If the input
-member is set to ``ENABLED``, the client MUST perform best-effort validation of
-checksum values returned in the HTTP response.
+member is set to ``ENABLED``, the client MUST perform validation of checksum
+returned in the HTTP response.
 
 A client MUST use the list of supported algorithms modeled in the
-``responseAlgorithms`` property to look up the checksum(s) for which validation
-MUST be performed. The client MUST look for the HTTP header in the response as
-per the :ref:`resolving checksum name <aws.protocols#httpChecksum-trait_resolving-checksum-name>`
-section.
+``responseAlgorithms`` property and filter it for the algorithms supported by
+the client to look up the checksum(s) for which validation MUST be performed.
+The client MUST look for the HTTP header in the response as per the
+:ref:`resolving checksum name <aws.protocols#httpChecksum-trait_resolving-checksum-name>`
+section. From the available headers in the response, the client MUST pick only
+one checksum to validate.
 
 A client MUST raise an error if the response checksum to validate does not
 match computed checksum of the response payload for the same checksum algorithm.
@@ -1098,7 +1117,12 @@ property are set, the service MUST validate an HTTP request checksum.
 When a service receives a request where the ``requestAlgorithmMember`` is set,
 the service MUST look up the input member represented by the checksum
 ``requestAlgorithmMember`` property. The value of this member is the checksum
-algorithm that the service MUST use to compute a checksum of the request payload.
+algorithm that the service MUST use to compute a checksum of the request
+payload. If the value of this member is not set, the service MUST look for the
+HTTP headers in the request as per the
+:ref:`resolving checksum name <aws.protocols#httpChecksum-trait_resolving-checksum-name>`
+section. From the available headers in the request, the service MUST pick only
+one checksum to validate.
 
 The computed checksum MUST be validated against the checksum supplied at a
 resolved location as per the :ref:`resolving checksum location
@@ -1114,7 +1138,7 @@ header.
 When using the ``httpChecksum`` trait, services MUST always accept checksum
 values in HTTP headers. For operations with streaming payload input, services
 MUST additionally accept checksum values sent in the `chunked trailer part`_ of
-the request body. Service MUST only send response checksums in HTTP headers.
+the request body.
 
 HTTP response checksums
 -----------------------
diff --git a/docs/source-2.0/guides/model-translations/converting-to-openapi.rst b/docs/source-2.0/guides/model-translations/converting-to-openapi.rst
index fdbaae7fe..81782ae64 100644
--- a/docs/source-2.0/guides/model-translations/converting-to-openapi.rst
+++ b/docs/source-2.0/guides/model-translations/converting-to-openapi.rst
@@ -1053,6 +1053,58 @@ disableIntEnums (``boolean``)
         }
 
 
+.. _generate-openapi-setting-addReferenceDescriptions:
+
+addReferenceDescriptions (``boolean``)
+    Set to ``true`` to add the ``description`` property to ``$ref`` members
+    with the value of the member's :ref:`documentation-trait` trait.
+
+    .. important::
+
+        This property is only supported when :ref:`version <generate-openapi-setting-version>`
+        is set to ``3.1.0``.
+
+    By default, ``$ref`` members will have no ``description``:
+
+    .. code-block:: smithy
+        :caption: example.smithy
+
+        structure Foo {
+            /// Member docs
+            bar: Bar
+        }
+
+    .. code-block:: json
+        :caption: Example.openapi.json
+
+        {
+            "Foo": {
+                "type": "object",
+                "properties": {
+                    "bar": {
+                        "$ref": "#/definitions/Bar"
+                    }
+                }
+            }
+        }
+
+    With this enabled, member docs will be added:
+
+    .. code-block:: json
+        :caption: Example.openapi.json
+
+        {
+            "Foo": {
+                "type": "object",
+                "properties": {
+                    "bar": {
+                        "$ref": "#/definitions/Bar",
+                        "description": "Member docs"
+                    }
+                }
+            }
+        }
+
 ----------------
 Security schemes
 ----------------
diff --git a/docs/source-2.0/guides/using-code-generation/generating-a-client.rst b/docs/source-2.0/guides/using-code-generation/generating-a-client.rst
index 2cef86578..9e76297dd 100644
--- a/docs/source-2.0/guides/using-code-generation/generating-a-client.rst
+++ b/docs/source-2.0/guides/using-code-generation/generating-a-client.rst
@@ -234,7 +234,7 @@ contents:
           "name": "weather-service",
           "scripts": {
             "generate": "cd smithy && gradle clean build",
-            "build": "yarn workspace @weather-service/client build",
+            "build": "yarn workspace @weather-service/client build"
           },
           "dependencies": {
             "@weather-service/client": "0.0.1"
diff --git a/docs/source-2.0/spec/json-ast.rst b/docs/source-2.0/spec/json-ast.rst
index 0f975d0e7..ddfefe401 100644
--- a/docs/source-2.0/spec/json-ast.rst
+++ b/docs/source-2.0/spec/json-ast.rst
@@ -414,8 +414,8 @@ shapes defined in JSON support the same properties as the Smithy IDL.
       - ``service``
     * - version
       - ``string``
-      - **Required**. Defines the version of the service. The version can be
-        provided in any format (e.g., ``2017-02-11``, ``2.0``, etc).
+      - Defines the version of the service. The version can be provided in any
+        format (e.g., ``2017-02-11``, ``2.0``, etc).
     * - :ref:`operations <service-operations>`
       - [:ref:`AST shape reference <ast-shape-reference>`]
       - Binds a list of operations to the service. Each reference MUST target
@@ -491,7 +491,7 @@ shapes defined in JSON support the same properties as the Smithy IDL.
       - Description
     * - type
       - string
-      - ``service``
+      - ``resource``
     * - :ref:`identifiers <resource-identifiers>`
       - Map<String, :ref:`AST shape reference <ast-shape-reference>`>
       - Defines identifier names and shape IDs used to identify the resource.
diff --git a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/auth/diff/SigV4Migration.java b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/auth/diff/SigV4Migration.java
index 890c9fda8..87e91abf1 100644
--- a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/auth/diff/SigV4Migration.java
+++ b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/auth/diff/SigV4Migration.java
@@ -83,23 +83,27 @@ public final class SigV4Migration extends AbstractDiffEvaluator {
             }
             // Validate Operation effective auth schemes
             for (ServiceShape newServiceShape : newOperationServiceBindings) {
-                ServiceShape oldServiceShape = oldModel.expectShape(newServiceShape.getId(), ServiceShape.class);
-                OperationShape oldOperationShape = change.getOldShape();
-                List<ShapeId> oldAuthSchemes = oldServiceIndex
-                    .getEffectiveAuthSchemes(oldServiceShape, oldOperationShape)
-                    .keySet()
-                    .stream()
-                    .collect(Collectors.toList());
-                List<ShapeId> newAuthSchemes = newServiceIndex
-                    .getEffectiveAuthSchemes(newServiceShape, newOperationShape)
-                    .keySet()
-                    .stream()
-                    .collect(Collectors.toList());
-                validateMigration(
-                    newOperationShape,
-                    oldAuthSchemes,
-                    newAuthSchemes,
-                    events);
+                oldModel.getShape(newServiceShape.getId())
+                    .filter(Shape::isServiceShape)
+                    .map(s -> (ServiceShape) s)
+                    .ifPresent(oldServiceShape -> {
+                        OperationShape oldOperationShape = change.getOldShape();
+                        List<ShapeId> oldAuthSchemes = oldServiceIndex
+                            .getEffectiveAuthSchemes(oldServiceShape, oldOperationShape)
+                            .keySet()
+                            .stream()
+                            .collect(Collectors.toList());
+                        List<ShapeId> newAuthSchemes = newServiceIndex
+                            .getEffectiveAuthSchemes(newServiceShape, newOperationShape)
+                            .keySet()
+                            .stream()
+                            .collect(Collectors.toList());
+                        validateMigration(
+                            newOperationShape,
+                            oldAuthSchemes,
+                            newAuthSchemes,
+                            events);
+                    });
             }
         }
 
diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/DeconflictingStrategy.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/DeconflictingStrategy.java
index ec4cf34c5..09f84243f 100644
--- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/DeconflictingStrategy.java
+++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/DeconflictingStrategy.java
@@ -29,6 +29,7 @@ import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.shapes.SimpleShape;
 import software.amazon.smithy.model.traits.EnumTrait;
 import software.amazon.smithy.model.traits.PrivateTrait;
+import software.amazon.smithy.model.transform.ModelTransformer;
 import software.amazon.smithy.utils.FunctionalUtils;
 import software.amazon.smithy.utils.StringUtils;
 
@@ -56,8 +57,14 @@ final class DeconflictingStrategy implements RefStrategy {
         this.delegate = delegate;
 
         // Pre-compute a map of all converted shape refs. Sort the shapes
-        // to make the result deterministic.
-        model.shapes().filter(shapePredicate.and(FunctionalUtils.not(this::isIgnoredShape))).sorted().forEach(shape -> {
+        // to make the result deterministic. Eliminate trait definitions
+        // ahead of that computation to eliminate potential conflicts with
+        // shapes that won't go into JSON Schema anyway.
+        Model scrubbedModel = ModelTransformer.create().scrubTraitDefinitions(model);
+        scrubbedModel.shapes()
+                .filter(shapePredicate.and(FunctionalUtils.not(this::isIgnoredShape)))
+                .sorted()
+                .forEach(shape -> {
             String pointer = delegate.toPointer(shape.getId());
             if (!reversePointers.containsKey(pointer)) {
                 pointers.put(shape.getId(), pointer);
diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java
index 6794a8c29..6656647ec 100644
--- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java
+++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java
@@ -114,6 +114,7 @@ public class JsonSchemaConfig {
     private boolean useIntegerType;
     private boolean disableDefaultValues = false;
     private boolean disableIntEnums = false;
+    private boolean addReferenceDescriptions = false;
 
     public JsonSchemaConfig() {
         nodeMapper.setWhenMissingSetter(NodeMapper.WhenMissing.IGNORE);
@@ -456,4 +457,26 @@ public class JsonSchemaConfig {
     public void setJsonSchemaVersion(JsonSchemaVersion schemaVersion) {
         this.jsonSchemaVersion = Objects.requireNonNull(schemaVersion);
     }
+
+    /**
+     * Whether to add the {@code description} property to Schema References
+     * when converting Smithy member shapes into JSON Schema with the value
+     * of the member's documentation.
+     *
+     * <p>Defaults to {@code false}.</p>
+     *
+     * @return Whether to add descriptions to Schema References.
+     */
+    public boolean getAddReferenceDescriptions() {
+        return addReferenceDescriptions;
+    }
+
+    /**
+     * Sets whether the {@code description} property should be added to Schema References.
+     *
+     * @param addReferenceDescriptions Whether to add descriptions to Schema References
+     */
+    public void setAddReferenceDescriptions(boolean addReferenceDescriptions) {
+        this.addReferenceDescriptions = addReferenceDescriptions;
+    }
 }
diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java
index 4f6898ff5..a6067d959 100644
--- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java
+++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java
@@ -106,6 +106,11 @@ final class JsonSchemaShapeVisitor extends ShapeVisitor.Default<Schema> {
             if (member.hasTrait(DeprecatedTrait.class) && getJsonSchemaVersion() != JsonSchemaVersion.DRAFT07) {
                 refBuilder.deprecated(true);
             }
+
+            if (converter.getConfig().getAddReferenceDescriptions()) {
+                descriptionMessage(member).ifPresent(refBuilder::description);
+            }
+
             // Wrap the ref and default in an allOf if disableDefaultValues has been not been disabled on config.
             if (member.hasTrait(DefaultTrait.class) && !converter.getConfig().getDisableDefaultValues()) {
                 Schema def = Schema.builder().defaultValue(member.expectTrait(DefaultTrait.class).toNode()).build();
diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java
index 94b19f12c..beddd9e43 100644
--- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java
+++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java
@@ -51,6 +51,7 @@ import software.amazon.smithy.model.transform.ModelTransformer;
 import software.amazon.smithy.model.validation.ValidationUtils;
 import software.amazon.smithy.openapi.OpenApiConfig;
 import software.amazon.smithy.openapi.OpenApiException;
+import software.amazon.smithy.openapi.OpenApiVersion;
 import software.amazon.smithy.openapi.model.ComponentsObject;
 import software.amazon.smithy.openapi.model.InfoObject;
 import software.amazon.smithy.openapi.model.OpenApi;
@@ -174,6 +175,12 @@ public final class OpenApiConverter {
             throw new OpenApiException("openapi is missing required property, `service`");
         }
 
+        if (config.getAddReferenceDescriptions() && config.getVersion() == OpenApiVersion.VERSION_3_0_2) {
+            throw new OpenApiException(
+                    "openapi property `addReferenceDescriptions` requires openapi version 3.1.0 or later.\n"
+                    + "Suggestion: Add `\"version\"`: \"3.1.0\" to your openapi config.");
+        }
+
         // Find the service shape.
         ServiceShape service = model.getShape(serviceShapeId)
                 .orElseThrow(() -> new IllegalArgumentException(String.format(


Test patch:
diff --git a/smithy-aws-protocol-tests/model/restJson1/http-content-type.smithy b/smithy-aws-protocol-tests/model/restJson1/http-content-type.smithy
index 3930bbeb5..d7def5d1c 100644
--- a/smithy-aws-protocol-tests/model/restJson1/http-content-type.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/http-content-type.smithy
@@ -283,7 +283,6 @@ apply TestGetNoPayload @httpRequestTests([
 /// These tests are to ensure we do not attach a body or related headers
 /// (Content-Type) to a POST operation with no modeled payload.
 ///
-@readonly
 @http(uri: "/no_payload", method: "POST")
 operation TestPostNoPayload {
     input: TestNoPayloadInputOutput,
@@ -363,7 +362,6 @@ apply TestGetNoInputNoPayload @httpRequestTests([
 /// These tests are to ensure we do not attach a body or related headers
 /// (Content-Type) to a POST operation with no modeled input.
 ///
-@readonly
 @http(uri: "/no_input_no_payload", method: "POST")
 operation TestPostNoInputNoPayload {
     output: TestNoPayloadInputOutput
diff --git a/smithy-aws-protocol-tests/model/restJson1/http-headers.smithy b/smithy-aws-protocol-tests/model/restJson1/http-headers.smithy
index 46eeafd0b..78fd9c909 100644
--- a/smithy-aws-protocol-tests/model/restJson1/http-headers.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/http-headers.smithy
@@ -424,11 +424,15 @@ operation NullAndEmptyHeadersClient {
 apply NullAndEmptyHeadersClient @httpRequestTests([
     {
         id: "RestJsonNullAndEmptyHeaders",
-        documentation: "Do not send null values, empty strings, or empty lists over the wire in headers",
+        documentation: "Do not send null values, but do send empty strings and empty lists over the wire in headers",
         protocol: restJson1,
         method: "GET",
         uri: "/NullAndEmptyHeadersClient",
-        forbidHeaders: ["X-A", "X-B", "X-C"],
+        forbidHeaders: ["X-A"],
+        headers: {
+            "X-B": ""
+            "X-C": ""
+        }
         body: "",
         params: {
             a: null,
diff --git a/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy b/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy
index 3dcdf401b..79e648b92 100644
--- a/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy
@@ -71,6 +71,7 @@ enum StringEnum {
         code: 200
     }
 ])
+@suppress(["UnstableTrait"])
 @httpMalformedRequestTests([
     {
         id: "RestJsonStringPayloadNoContentType",
diff --git a/smithy-aws-protocol-tests/model/restXml/content-type.smithy b/smithy-aws-protocol-tests/model/restXml/content-type.smithy
index 4dc9c578e..0559bf1c4 100644
--- a/smithy-aws-protocol-tests/model/restXml/content-type.smithy
+++ b/smithy-aws-protocol-tests/model/restXml/content-type.smithy
@@ -10,6 +10,7 @@ use smithy.test#httpRequestTests
 
 /// The example tests how servers must support requests
 /// containing a `Content-Type` header with parameters.
+@idempotent
 @http(uri: "/ContentTypeParameters", method: "PUT")
 operation ContentTypeParameters {
     input: ContentTypeParametersInput,
diff --git a/smithy-aws-protocol-tests/model/restXml/http-headers.smithy b/smithy-aws-protocol-tests/model/restXml/http-headers.smithy
index fb61681f5..2bf49ffc9 100644
--- a/smithy-aws-protocol-tests/model/restXml/http-headers.smithy
+++ b/smithy-aws-protocol-tests/model/restXml/http-headers.smithy
@@ -367,11 +367,15 @@ operation NullAndEmptyHeadersClient {
 apply NullAndEmptyHeadersClient @httpRequestTests([
     {
         id: "NullAndEmptyHeaders",
-        documentation: "Do not send null values, empty strings, or empty lists over the wire in headers",
+        documentation: "Do not send null values, but do send empty strings and empty lists over the wire in headers",
         protocol: restXml,
         method: "GET",
         uri: "/NullAndEmptyHeadersClient",
-        forbidHeaders: ["X-A", "X-B", "X-C"],
+        forbidHeaders: ["X-A"],
+        headers: {
+            "X-B": ""
+            "X-C": ""
+        }
         body: "",
         params: {
             a: null,
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/diffs/no-old-sigv4-new-service.a.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/diffs/no-old-sigv4-new-service.a.smithy
new file mode 100644
index 000000000..e75d0177d
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/diffs/no-old-sigv4-new-service.a.smithy
@@ -0,0 +1,21 @@
+$version: "2.0"
+
+metadata suppressions = [
+    {
+        id: "ModifiedTrait.Update.smithy.api#auth"
+        namespace: "ns.foo"
+    }
+]
+
+namespace ns.foo
+
+@auth([])
+@httpBearerAuth
+service Service {
+    operations: [
+        Operation
+    ]
+}
+
+@auth([])
+operation Operation {}
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/diffs/no-old-sigv4-new-service.b.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/diffs/no-old-sigv4-new-service.b.smithy
new file mode 100644
index 000000000..44870b7e7
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/diffs/no-old-sigv4-new-service.b.smithy
@@ -0,0 +1,30 @@
+$version: "2.0"
+
+metadata suppressions = [
+    {
+        id: "ModifiedTrait.Update.smithy.api#auth"
+        namespace: "ns.foo"
+    }
+]
+
+namespace ns.foo
+
+@auth([httpBearerAuth])
+@httpBearerAuth
+service Service {
+    operations: [
+        Operation
+    ]
+}
+
+@auth([httpBearerAuth])
+@httpBearerAuth
+service NewService {
+    operations: [
+        Operation
+    ]
+}
+
+
+@auth([httpBearerAuth])
+operation Operation {}
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/diffs/no-old-sigv4-new-service.events b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/diffs/no-old-sigv4-new-service.events
new file mode 100644
index 000000000..7ebdd3d2d
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/diffs/no-old-sigv4-new-service.events
@@ -0,0 +1,1 @@
+[NOTE] ns.foo#NewService: Added service `ns.foo#NewService` | AddedShape
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-trait.errors b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-trait.errors
index 750e98437..361bfb9f0 100644
--- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-trait.errors
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-trait.errors
@@ -3,5 +3,6 @@
 [SUPPRESSED] smithy.example#NoOutputForResponse: This shape applies a trait that is unstable: aws.protocols#httpChecksum | UnstableTrait
 [SUPPRESSED] smithy.example#NoResponseAlgorithms: This shape applies a trait that is unstable: aws.protocols#httpChecksum | UnstableTrait
 [ERROR] smithy.example#NoBehavior: The `httpChecksum` trait must define at least one of the `request` or `response` checksum behaviors. | HttpChecksumTrait
+[ERROR] smithy.example#NoBehaviorRequestChecksumRequiredFalse: The `httpChecksum` trait must define at least one of the `request` or `response` checksum behaviors. | HttpChecksumTrait
 [ERROR] smithy.example#NoModeForResponse: The `httpChecksum` trait must model the `requestValidationModeMember` property to support response checksum behavior. | HttpChecksumTrait
 [ERROR] smithy.example#NoResponseAlgorithms: The `httpChecksum` trait must model the `responseAlgorithms` property to support response checksum behavior. | HttpChecksumTrait
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-trait.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-trait.smithy
index 24ce4cf32..4710c018c 100644
--- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-trait.smithy
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-trait.smithy
@@ -14,6 +14,18 @@ operation NoBehavior {
 @input
 structure NoBehaviorInput {}
 
+@httpChecksum(
+    requestChecksumRequired: false,
+)
+@suppress(["UnstableTrait"])
+operation NoBehaviorRequestChecksumRequiredFalse {
+    input: NoBehaviorRequestChecksumRequiredFalseInput,
+    output: Unit
+}
+
+@input
+structure NoBehaviorRequestChecksumRequiredFalseInput {}
+
 @httpChecksum(
     requestChecksumRequired: true,
 )
diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DeconflictingStrategyTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DeconflictingStrategyTest.java
index 2927c9be2..a870b9b70 100644
--- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DeconflictingStrategyTest.java
+++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DeconflictingStrategyTest.java
@@ -13,6 +13,8 @@ import software.amazon.smithy.model.shapes.MemberShape;
 import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.shapes.StringShape;
 import software.amazon.smithy.model.shapes.StructureShape;
+import software.amazon.smithy.model.traits.PrivateTrait;
+import software.amazon.smithy.model.traits.TraitDefinition;
 
 public class DeconflictingStrategyTest {
     @Test
@@ -97,4 +99,27 @@ public class DeconflictingStrategyTest {
             .createDefaultStrategy(model, new JsonSchemaConfig(), propertyNamingStrategy, alwaysTrue());
         assertThat(strategy.toPointer(a.getId()), equalTo("#/definitions/Severity"));
     }
+
+    @Test
+    public void excludesTraitDefinitions() {
+        StringShape member = StringShape.builder().id("com.foo#String").build();
+        StructureShape matcher = StructureShape.builder()
+                .id("com.foo#Matcher")
+                .addMember("member", member.getId())
+                .build();
+        StructureShape matcherForTrait = StructureShape.builder()
+                .id("com.bar#Matcher")
+                .addTrait(new PrivateTrait())
+                .build();
+        StructureShape trait = StructureShape.builder()
+                .id("com.bar#Trait")
+                .addTrait(TraitDefinition.builder().build())
+                .addMember("matcher", matcherForTrait.toShapeId())
+                .build();
+        Model model = Model.assembler().addShapes(trait, matcherForTrait, matcher, member).assemble().unwrap();
+        PropertyNamingStrategy propertyNamingStrategy = PropertyNamingStrategy.createDefaultStrategy();
+        RefStrategy strategy = RefStrategy
+                .createDefaultStrategy(model, new JsonSchemaConfig(), propertyNamingStrategy, alwaysTrue());
+        assertThat(strategy.toPointer(matcher.getId()), equalTo("#/definitions/Matcher"));
+    }
 }
diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java
index 21410679d..9a9a0f3a5 100644
--- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java
+++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java
@@ -887,4 +887,24 @@ public class JsonSchemaConverterTest {
         Schema memberSchema = document.getRootSchema().getProperties().get("member");
         assertThat(memberSchema.isDeprecated(), equalTo(false));
     }
+
+    @Test
+    public void canAddMemberDocumentation() {
+        Model model = Model.assembler()
+                .addImport(getClass().getResource("member-documentation.smithy"))
+                .assemble()
+                .unwrap();
+
+        JsonSchemaConfig config = new JsonSchemaConfig();
+        config.setAddReferenceDescriptions(true);
+        SchemaDocument document = JsonSchemaConverter.builder()
+                .config(config)
+                .model(model)
+                .build()
+                .convert();
+
+        Node expected = Node.parse(
+                IoUtils.toUtf8String(getClass().getResourceAsStream("member-documentation.jsonschema.json")));
+        Node.assertEquals(document.toNode(), expected);
+    }
 }
diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/member-documentation.jsonschema.json b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/member-documentation.jsonschema.json
new file mode 100644
index 000000000..d2b622cc9
--- /dev/null
+++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/member-documentation.jsonschema.json
@@ -0,0 +1,20 @@
+{
+    "definitions": {
+        "Foo": {
+            "type": "object",
+            "properties": {
+                "foo": {
+                    "type": "string",
+                    "description": "simple docs"
+                },
+                "bar": {
+                    "$ref": "#/definitions/Bar",
+                    "description": "structure docs"
+                }
+            }
+        },
+        "Bar": {
+            "type": "object"
+        }
+    }
+}
diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/member-documentation.smithy b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/member-documentation.smithy
new file mode 100644
index 000000000..edf4215de
--- /dev/null
+++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/member-documentation.smithy
@@ -0,0 +1,13 @@
+$version: "2.0"
+
+namespace smithy.example
+
+structure Foo {
+    /// simple docs
+    foo: String
+
+    /// structure docs
+    bar: Bar
+}
+
+structure Bar {}
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 2c3fcdc80..efe4a0968 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
@@ -22,6 +22,7 @@ import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.not;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.util.Collections;
 import java.util.List;
@@ -639,4 +640,37 @@ public class OpenApiConverterTest {
 
         Node.assertEquals(result, expectedNode);
     }
+
+    @Test
+    public void convertsMemberDocumentation() {
+        Model model = Model.assembler()
+                .addImport(getClass().getResource("documentation-test-members.smithy"))
+                .discoverModels()
+                .assemble()
+                .unwrap();
+        OpenApiConfig config = new OpenApiConfig();
+        config.setService(ShapeId.from("smithy.example#MyDocs"));
+        config.setVersion(OpenApiVersion.VERSION_3_1_0);
+        config.setAddReferenceDescriptions(true);
+        Node result = OpenApiConverter.create().config(config).convertToNode(model);
+        Node expectedNode = Node.parse(IoUtils.toUtf8String(
+                getClass().getResourceAsStream("documentation-test-members.openapi.json")));
+
+        Node.assertEquals(result, expectedNode);
+    }
+
+    @Test
+    public void convertingMemberDocsRequired3_1() {
+        Model model = Model.assembler()
+                .addImport(getClass().getResource("documentation-test-members.smithy"))
+                .discoverModels()
+                .assemble()
+                .unwrap();
+        OpenApiConfig config = new OpenApiConfig();
+        config.setService(ShapeId.from("smithy.example#MyDocs"));
+        config.setAddReferenceDescriptions(true);
+        OpenApiConverter converter = OpenApiConverter.create().config(config);
+
+        assertThrows(OpenApiException.class, () -> converter.convertToNode(model));
+    }
 }
diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/documentation-test-members.openapi.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/documentation-test-members.openapi.json
new file mode 100644
index 000000000..08f239aae
--- /dev/null
+++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/documentation-test-members.openapi.json
@@ -0,0 +1,55 @@
+{
+    "openapi": "3.1.0",
+    "info": {
+        "title": "MyDocs",
+        "version": "2018-01-01",
+        "description": "Service"
+    },
+    "paths": {
+        "/": {
+            "get": {
+                "description": "Operation",
+                "operationId": "MyDocsOperation",
+                "responses": {
+                    "200": {
+                        "description": "MyDocsOperation 200 response",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/MyDocsOperationResponseContent"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    },
+    "components": {
+        "schemas": {
+            "MyDocsOperationResponseContent": {
+                "type": "object",
+                "description": "Output",
+                "properties": {
+                    "foo": {
+                        "type": "string",
+                        "description": "foo member."
+                    },
+                    "nested": {
+                        "$ref": "#/components/schemas/Nested",
+                        "description": "nested member."
+                    }
+                }
+            },
+            "Nested": {
+                "type": "object",
+                "description": "Nested",
+                "properties": {
+                    "baz": {
+                        "type": "string"
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/documentation-test-members.smithy b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/documentation-test-members.smithy
new file mode 100644
index 000000000..789f43c38
--- /dev/null
+++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/documentation-test-members.smithy
@@ -0,0 +1,31 @@
+$version: "2.0"
+
+namespace smithy.example
+
+/// Service
+@aws.protocols#restJson1
+service MyDocs {
+    version: "2018-01-01",
+    operations: [MyDocsOperation]
+}
+
+/// Operation
+@http(method: "GET", uri: "/")
+@readonly
+operation MyDocsOperation {
+    output: Output
+}
+
+/// Output
+structure Output {
+    /// foo member.
+    foo: String,
+
+    /// nested member.
+    nested: Nested,
+}
+
+/// Nested
+structure Nested {
+    baz: String,
+}