Repository: smithy-lang/smithy
Pull request #2236 (2024-04-09 18:35:28)

Nested xml map with xml name
#### Background
* What do these changes do? 
Adds a protocol test for the restXml protocol for unmarshalling a response that contains a nested xml map with the `@xmlName `trait applied to both key and value pairs

* Why are they important?
While implementing other protocol tests we realized that this was a gap in our generator, but it was hard to fix without an existing protocol test. This drives TDD for us.


#### Testing
* How did you test these changes?
```
./gradlew :smithy-aws-protocol-tests:build 
PS C:\Dev\Repos\smithy-fork> ./gradlew :smithy-aws-protocol-tests:build
Picked up JAVA_TOOL_OPTIONS: -Dlog4j2.formatMsgNoLookups=true

> Configure project :
Smithy version: '1.47.0'

> Task :smithy-validation-model:smithyFormat
Running smithy format

> Task :smithy-aws-protocol-tests:smithyBuild
Running smithy build
SUCCESS: Validated 2831 shapes

Validated model, now starting projections...

ΓöÇΓöÇ  source  ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ
Completed projection source (2831): C:\Dev\Repos\smithy-fork\smithy-aws-protocol-tests\build\smithyprojections\smithy-aws-protocol-tests\source

Summary: Smithy built 1 projection(s), 3 plugin(s), and 134 artifacts

> Task :smithy-aws-protocol-tests:smithyJarValidate
Running smithy validate
SUCCESS: Validated 2831 shapes


Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.6/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 4s
36 actionable tasks: 8 executed, 28 up-to-date
```
#### Links
* Links to additional context, if necessary
* Issue #, if applicable (see [here](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for a list of keywords to use for linking issues)

---
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 de2607dba..b8385815b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,7 @@ jobs:
 
     steps:
       - uses: actions/checkout@v4
-      - uses: gradle/wrapper-validation-action@v2
+      - uses: gradle/wrapper-validation-action@v3
 
       - name: Set up JDK ${{ matrix.java }}
         uses: actions/setup-java@v4
diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml
new file mode 100644
index 000000000..3d3d7333d
--- /dev/null
+++ b/.github/workflows/post-release.yml
@@ -0,0 +1,29 @@
+# Runs certain jobs after a release is published or on an as-needed basis (manually),
+# e.g. triggering an update to the homebrew-tap when a new version of the CLI is available
+
+name: post-release
+on:
+  release:
+    types: [released]
+  workflow_dispatch: # on button click
+    inputs:
+      version:
+        description: 'The version that was released (e.g. 1.47.0)'
+        required: true
+        type: string
+
+env:
+  version: ${{ github.event.inputs.version || github.ref_name }}
+
+jobs:
+  homebrew-tap:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Trigger homebrew-tap update 
+      uses: benc-uk/workflow-dispatch@v1
+      with:
+        workflow: autoupdate
+        repo: smithy-lang/homebrew-tap
+        inputs: '{ "version": "${{ env.version }}" }'
+        # Required when using the `repo` option
+        token: "${{ secrets.PR_TOKEN }}"
\ No newline at end of file
diff --git a/.github/workflows/update-brew.yml b/.github/workflows/update-brew.yml
deleted file mode 100644
index 9a86b65de..000000000
--- a/.github/workflows/update-brew.yml
+++ /dev/null
@@ -1,118 +0,0 @@
-name: update-brew
-on:
-  release:
-    types: [released]
-  workflow_dispatch: # on button click
-
-env:
-  release_prefix: https://github.com/smithy-lang/smithy/releases/download
-
-jobs:
-  download-and-save:
-    runs-on: ubuntu-latest
-    steps:
-      - name: mac
-        if: startsWith(github.ref, 'refs/tags/')
-        run: |
-          version="${GITHUB_REF##*/}"
-          hash_url="${{ env.release_prefix }}/${version}/smithy-cli-darwin-x86_64.tar.gz"
-          echo $hash_url
-          curl -fJLO $hash_url.sha256
-      - name: mac-arm
-        if: startsWith(github.ref, 'refs/tags/')
-        run: |
-          version="${GITHUB_REF##*/}"
-          hash_url="${{ env.release_prefix }}/${version}/smithy-cli-darwin-aarch64.tar.gz"
-          echo $hash_url
-          curl -fJLO $hash_url.sha256
-      - name: linux
-        if: startsWith(github.ref, 'refs/tags/')
-        run: |
-          version="${GITHUB_REF##*/}"
-          hash_url="${{ env.release_prefix }}/${version}/smithy-cli-linux-x86_64.tar.gz"
-          echo $hash_url
-          curl -fJLO $hash_url.sha256
-      - name: linux-arm
-        if: startsWith(github.ref, 'refs/tags/')
-        run: |
-          version="${GITHUB_REF##*/}"
-          hash_url="${{ env.release_prefix }}/${version}/smithy-cli-linux-aarch64.tar.gz"
-          echo $hash_url
-          curl -fJLO $hash_url.sha256
-      - name: Save artifacts
-        uses: actions/upload-artifact@v4
-        with:
-          name: brew-artifacts
-          path: '*.sha256'
-          retention-days: 7
-
-  create-pr:
-    runs-on: ubuntu-latest
-    needs: download-and-save
-    permissions:
-      pull-requests: write
-    steps:
-      - name: Get artifacts
-        id: download
-        uses: actions/download-artifact@v4
-        with:
-          name: brew-artifacts
-      - name: Checkout bottle repo
-        uses: actions/checkout@v4
-        with:
-          repository: 'smithy-lang/homebrew-tap'
-          path: 'homebrew-tap'
-          ref: 'main'
-          token: ${{ secrets.PR_TOKEN }}
-      - name: Update version
-        run: |
-          version="${GITHUB_REF##*/}"
-          tmp=$(mktemp)
-          jq --arg version "${version}" '.version = $version' homebrew-tap/bottle-configs/smithy-cli.json > "$tmp" && mv "$tmp" homebrew-tap/bottle-configs/smithy-cli.json
-      - name: Update root_url
-        run: |
-          version="${GITHUB_REF##*/}"
-          tmp=$(mktemp)
-          jq --arg release_url "${{ env.release_prefix }}" --arg version "${version}" '.bottle.root_url = "\($release_url)/\($version)/smithy-cli"' homebrew-tap/bottle-configs/smithy-cli.json > "$tmp" && mv "$tmp" homebrew-tap/bottle-configs/smithy-cli.json
-      - name: Update mac
-        run: |
-          version="${GITHUB_REF##*/}"
-          sha=$(cut -d " " -f1 ${{steps.download.outputs.download-path}}/smithy-cli-darwin-x86_64.tar.gz.sha256)
-          tmp=$(mktemp)
-          jq --arg sha "$sha" '.bottle.sha256.sierra = "'$sha'"' homebrew-tap/bottle-configs/smithy-cli.json > "$tmp" && mv "$tmp" homebrew-tap/bottle-configs/smithy-cli.json
-      - name: Update mac-arm
-        run: |
-          version="${GITHUB_REF##*/}"
-          sha=$(cut -d " " -f1 ${{steps.download.outputs.download-path}}/smithy-cli-darwin-aarch64.tar.gz.sha256)
-          tmp=$(mktemp)
-          jq --arg sha "$sha" '.bottle.sha256.arm64_big_sur = "'$sha'"' homebrew-tap/bottle-configs/smithy-cli.json > "$tmp" && mv "$tmp" homebrew-tap/bottle-configs/smithy-cli.json
-      - name: Update linux
-        run: |
-          version="${GITHUB_REF##*/}"
-          sha=$(cut -d " " -f1 ${{steps.download.outputs.download-path}}/smithy-cli-linux-x86_64.tar.gz.sha256)
-          tmp=$(mktemp)
-          jq --arg sha "$sha" '.bottle.sha256.linux = "'$sha'"' homebrew-tap/bottle-configs/smithy-cli.json > "$tmp" && mv "$tmp" homebrew-tap/bottle-configs/smithy-cli.json
-      - name: Update linux-arm
-        run: |
-          version="${GITHUB_REF##*/}"
-          sha=$(cut -d " " -f1 ${{steps.download.outputs.download-path}}/smithy-cli-linux-aarch64.tar.gz.sha256)
-          tmp=$(mktemp)
-          jq --arg sha "$sha" '.bottle.sha256.linux_arm = "'$sha'"' homebrew-tap/bottle-configs/smithy-cli.json > "$tmp" && mv "$tmp" homebrew-tap/bottle-configs/smithy-cli.json
-      - name: Set up new git branch for version bump
-        working-directory: homebrew-tap
-        run: |
-          git config user.name 'smithy-automation'
-          git config user.email 'github-smithy-automation@amazon.com'
-          git checkout -b "automation/bump-smithy-cli-version/${GITHUB_REF##*/}"
-      - name: Create PR
-        working-directory: homebrew-tap
-        run: |
-          git add bottle-configs/smithy-cli.json
-          git commit -m "chore: upgrade smithy-cli to ${GITHUB_REF##*/}"
-          git push --set-upstream origin "automation/bump-smithy-cli-version/${GITHUB_REF##*/}"
-          gh pr create \
-            --title "[Automation] chore: upgrade smithy-cli to ${GITHUB_REF##*/}" \
-            --body "Created by ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
-            --base main
-        env:
-          GITHUB_TOKEN: ${{ secrets.PR_TOKEN }}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 427c39628..96aea2704 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -78,3 +78,12 @@ If you discover a potential security issue in this project we ask that you notif
 ## Licensing
 
 See the [LICENSE](https://github.com/smithy-lang/smithy/blob/main/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
+
+Please use the following license header in new code files:
+
+```
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+```
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 46550dfff..4f6898ff5 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
@@ -18,6 +18,7 @@ package software.amazon.smithy.jsonschema;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
 import software.amazon.smithy.model.Model;
@@ -43,6 +44,7 @@ import software.amazon.smithy.model.shapes.StructureShape;
 import software.amazon.smithy.model.shapes.TimestampShape;
 import software.amazon.smithy.model.shapes.UnionShape;
 import software.amazon.smithy.model.traits.DefaultTrait;
+import software.amazon.smithy.model.traits.DeprecatedTrait;
 import software.amazon.smithy.model.traits.DocumentationTrait;
 import software.amazon.smithy.model.traits.EnumTrait;
 import software.amazon.smithy.model.traits.LengthTrait;
@@ -100,13 +102,16 @@ final class JsonSchemaShapeVisitor extends ShapeVisitor.Default<Schema> {
         if (converter.isInlined(member)) {
             return member.accept(this);
         } else {
+            Schema.Builder refBuilder = Schema.builder().ref(converter.toPointer(member.getTarget()));
+            if (member.hasTrait(DeprecatedTrait.class) && getJsonSchemaVersion() != JsonSchemaVersion.DRAFT07) {
+                refBuilder.deprecated(true);
+            }
             // 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 ref = Schema.builder().ref(converter.toPointer(member.getTarget())).build();
                 Schema def = Schema.builder().defaultValue(member.expectTrait(DefaultTrait.class).toNode()).build();
-                return Schema.builder().allOf(ListUtils.of(ref, def)).build();
+                return Schema.builder().allOf(ListUtils.of(refBuilder.build(), def)).build();
             }
-            return Schema.builder().ref(converter.toPointer(member.getTarget())).build();
+            return refBuilder.build();
         }
     }
 
@@ -274,9 +279,7 @@ final class JsonSchemaShapeVisitor extends ShapeVisitor.Default<Schema> {
      * @return Returns the updated schema builder.
      */
     private Schema.Builder updateBuilder(Shape shape, Schema.Builder builder) {
-        shape.getMemberTrait(model, DocumentationTrait.class)
-                .map(DocumentationTrait::getValue)
-                .ifPresent(builder::description);
+        descriptionMessage(shape).ifPresent(builder::description);
 
         shape.getTrait(TitleTrait.class)
                 .map(TitleTrait::getValue)
@@ -329,9 +332,31 @@ final class JsonSchemaShapeVisitor extends ShapeVisitor.Default<Schema> {
             builder.defaultValue(shape.expectTrait(DefaultTrait.class).toNode());
         }
 
+        if (shape.hasTrait(DeprecatedTrait.class) && getJsonSchemaVersion() != JsonSchemaVersion.DRAFT07) {
+            builder.deprecated(true);
+        }
+
         return builder;
     }
 
+    private Optional<String> descriptionMessage(Shape shape) {
+        StringBuilder builder = new StringBuilder();
+        shape
+            .getTrait(DocumentationTrait.class)
+            .ifPresent(trait ->
+                builder.append(trait.getValue())
+            );
+        shape
+            .getTrait(DeprecatedTrait.class)
+            .ifPresent(trait ->
+                builder
+                    .append("\n")
+                    .append(trait.getDeprecatedDescription(shape.getType()))
+            );
+        String description = builder.toString().trim();
+        return description.isEmpty() ? Optional.empty() : Optional.of(description);
+    }
+
     /**
      * Builds a schema builder and applied schema mappers.
      *
@@ -351,4 +376,8 @@ final class JsonSchemaShapeVisitor extends ShapeVisitor.Default<Schema> {
 
         return builder.build();
     }
+
+    private JsonSchemaVersion getJsonSchemaVersion() {
+        return converter.getConfig().getJsonSchemaVersion();
+    }
 }
diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java
index 08a33ccf8..7d820b970 100644
--- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java
+++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java
@@ -102,6 +102,7 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
     private final boolean writeOnly;
     private final String comment;
     private final Node examples;
+    private final boolean deprecated;
 
     private final String contentEncoding;
     private final String contentMediaType;
@@ -153,6 +154,7 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
         writeOnly = builder.writeOnly;
         comment = builder.comment;
         examples = builder.examples;
+        deprecated = builder.deprecated;
 
         contentEncoding = builder.contentEncoding;
         contentMediaType = builder.contentMediaType;
@@ -312,6 +314,10 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
         return Optional.ofNullable(examples);
     }
 
+    public boolean isDeprecated() {
+        return deprecated;
+    }
+
     public Optional<String> getContentEncoding() {
         return Optional.ofNullable(contentEncoding);
     }
@@ -364,6 +370,7 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
 
                 .withOptionalMember("comment", getComment().map(Node::from))
                 .withOptionalMember("examples", getExamples())
+                .withOptionalMember("deprecated", this.deprecated ? Optional.of(Node.from(true)) : Optional.empty())
                 .withOptionalMember("title", getTitle().map(Node::from))
                 .withOptionalMember("description", getDescription().map(Node::from))
                 .withOptionalMember("format", getFormat().map(Node::from))
@@ -421,6 +428,10 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
             result.withMember("writeOnly", Node.from(true));
         }
 
+        if (deprecated) {
+            result.withMember("deprecated", Node.from(true));
+        }
+
         for (Map.Entry<String, ToNode> entry : extensions.entrySet()) {
             result.withMember(entry.getKey(), entry.getValue().toNode());
         }
@@ -540,6 +551,7 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
                 .writeOnly(writeOnly)
                 .comment(comment)
                 .examples(examples)
+                .deprecated(deprecated)
 
                 .contentEncoding(contentEncoding)
                 .contentMediaType(contentMediaType);
@@ -611,6 +623,7 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
         private boolean writeOnly;
         private String comment;
         private Node examples;
+        private boolean deprecated;
 
         private String contentEncoding;
         private String contentMediaType;
@@ -853,6 +866,11 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
             return this;
         }
 
+        public Builder deprecated(boolean deprecated) {
+            this.deprecated = deprecated;
+            return this;
+        }
+
         public Builder extensions(Map<String, Node> extensions) {
             this.extensions.clear();
             this.extensions.putAll(extensions);
@@ -945,6 +963,8 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
                     return this.contentMediaType(null);
                 case "examples":
                     return this.examples(null);
+                case "deprecated":
+                    return this.deprecated(false);
                 default:
                     LOGGER.warning("Unknown JSON Schema config 'disable' property: " + propertyName);
                     return this;
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/DeprecatedTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/DeprecatedTrait.java
index d5b0b6ffa..d960b5d19 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/DeprecatedTrait.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/DeprecatedTrait.java
@@ -19,6 +19,7 @@ import java.util.Optional;
 import software.amazon.smithy.model.node.Node;
 import software.amazon.smithy.model.node.ObjectNode;
 import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.shapes.ShapeType;
 import software.amazon.smithy.utils.MapUtils;
 import software.amazon.smithy.utils.ToSmithyBuilder;
 
@@ -72,6 +73,20 @@ public final class DeprecatedTrait extends AbstractTrait implements ToSmithyBuil
         return Optional.ofNullable(message);
     }
 
+    public String getDeprecatedDescription(ShapeType shapeType) {
+        StringBuilder builder = new StringBuilder("This ");
+        builder.append(shapeType == ShapeType.OPERATION ? "operation" : "shape").append(" is deprecated");
+        if (since != null) {
+            builder.append(" since ").append(since);
+        }
+        if (message != null) {
+            builder.append(": ").append(message);
+        } else {
+            builder.append(".");
+        }
+        return builder.toString();
+    }
+
     @Override
     protected Node createNode() {
         return new ObjectNode(MapUtils.of(), getSourceLocation())
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 f4cc38035..abce5303e 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
@@ -218,7 +218,7 @@ structure protocolDefinition {
     traits: TraitShapeIdList
 
     /// Set to true if inline documents are not supported by this protocol.
-    @deprecated(message: "Use the `@constrainShapes` trait instead")
+    @deprecated(message: "Use the `@traitValidators` trait instead")
     noInlineDocumentSupport: Boolean
 }
 


Test patch:
diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/simple-service-aws.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/simple-service-aws.cfn.json
index 2c61b7979..e0d8e2ffe 100644
--- a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/simple-service-aws.cfn.json
+++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/simple-service-aws.cfn.json
@@ -19,7 +19,8 @@
   },
   "properties": {
     "FooDeprecatedMutableProperty": {
-      "type": "string"
+      "type": "string",
+      "description": "This shape is deprecated: Use the `fooValidFullyMutableProperty` property."
     },
     "FooId": {
       "type": "string"
diff --git a/smithy-aws-protocol-tests/model/restXml/document-maps.smithy b/smithy-aws-protocol-tests/model/restXml/document-maps.smithy
index fc2541a03..0062b98b9 100644
--- a/smithy-aws-protocol-tests/model/restXml/document-maps.smithy
+++ b/smithy-aws-protocol-tests/model/restXml/document-maps.smithy
@@ -650,6 +650,151 @@ apply NestedXmlMaps @httpResponseTests([
     },
 ])
 
+/// Nested Xml Maps with key/values with @xmlName
+@http(uri: "/NestedXmlMapWithXmlName", method: "POST")
+operation NestedXmlMapWithXmlName {
+    input: NestedXmlMapWithXmlNameInputOutput
+    output: NestedXmlMapWithXmlNameInputOutput
+}
+
+structure NestedXmlMapWithXmlNameInputOutput {
+    nestedXmlMapWithXmlNameMap: NestedXmlMapWithXmlNameMap
+}
+
+map NestedXmlMapWithXmlNameMap{
+    @xmlName("OuterKey")
+    key: String
+    
+    value: NestedXmlMapWithXmlNameInnerMap
+}
+
+map NestedXmlMapWithXmlNameInnerMap{
+    @xmlName("InnerKey")
+    key: String
+    
+    @xmlName("InnerValue")
+    value: String
+}
+
+apply NestedXmlMapWithXmlName @httpRequestTests([
+    {
+        id : "NestedXmlMapWithXmlNameSerializes",
+        documentation : "Serializes nested XML Maps in requests that have xmlName on members",
+        protocol: restXml,
+        method: "POST",
+        uri: "/NestedXmlMapWithXmlName",
+        body: """
+            <NestedXmlMapWithXmlNameRequest>
+                <nestedXmlMapWithXmlNameMap>
+                    <entry>
+                        <OuterKey>foo</OuterKey>
+                        <value>
+                            <entry>
+                                <InnerKey>bar</InnerKey>
+                                <InnerValue>Baz</InnerValue>
+                            </entry>
+                            <entry>
+                                <InnerKey>fizz</InnerKey>
+                                <InnerValue>Buzz</InnerValue>
+                            </entry>
+                        </value>
+                    </entry>
+                    <entry>
+                        <OuterKey>qux</OuterKey>
+                        <value>
+                            <entry>
+                                <InnerKey>foobar</InnerKey>
+                                <InnerValue>Bar</InnerValue>
+                            </entry>
+                            <entry>
+                                <InnerKey>fizzbuzz</InnerKey>
+                                <InnerValue>Buzz</InnerValue>
+                            </entry>
+                        </value>
+                    </entry>
+                </nestedXmlMapWithXmlNameMap>
+            </NestedXmlMapWithXmlNameRequest>
+        """,
+        bodyMediaType: "application/xml",
+        headers: {
+            "Content-Type": "application/xml",
+        },
+        params: {
+            nestedXmlMapWithXmlNameMap: {
+                foo: {
+                    bar: "Baz",
+                    fizz: "Buzz"
+                },
+                qux: {
+                    foobar: "Bar",
+                    fizzbuzz: "Buzz"
+                }
+            }
+        }
+    }
+
+
+
+
+])
+
+apply NestedXmlMapWithXmlName @httpResponseTests([
+    {
+        id: "NestedXmlMapWithXmlNameDeserializes",
+        documentation: "Serializes nested XML maps in responses that have xmlName on members",
+        protocol: restXml,
+        code: 200,
+        body: """
+            <NestedXmlMapWithXmlNameResponse>
+                <nestedXmlMapWithXmlNameMap>
+                    <entry>
+                        <OuterKey>foo</OuterKey>
+                        <value>
+                            <entry>
+                                <InnerKey>bar</InnerKey>
+                                <InnerValue>Baz</InnerValue>
+                            </entry>
+                            <entry>
+                                <InnerKey>fizz</InnerKey>
+                                <InnerValue>Buzz</InnerValue>
+                            </entry>
+                        </value>
+                    </entry>
+                    <entry>
+                        <OuterKey>qux</OuterKey>
+                        <value>
+                            <entry>
+                                <InnerKey>foobar</InnerKey>
+                                <InnerValue>Bar</InnerValue>
+                            </entry>
+                            <entry>
+                                <InnerKey>fizzbuzz</InnerKey>
+                                <InnerValue>Buzz</InnerValue>
+                            </entry>
+                        </value>
+                    </entry>
+                </nestedXmlMapWithXmlNameMap>
+            </NestedXmlMapWithXmlNameResponse>
+        """
+        bodyMediaType: "application/xml",
+        headers: {
+            "Content-Type": "application/xml"
+        }
+        params: {
+            nestedXmlMapWithXmlNameMap: {
+                foo: {
+                    bar: "Baz",
+                    fizz: "Buzz"
+                },
+                qux: {
+                    foobar: "Bar",
+                    fizzbuzz: "Buzz"
+                }
+            }
+        }
+    }
+])
+
 /// Maps with @xmlNamespace and @xmlName
 @http(uri: "/XmlMapWithXmlNamespace", method: "POST")
 operation XmlMapWithXmlNamespace {
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 c65c8b9b3..21410679d 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
@@ -54,6 +54,7 @@ import software.amazon.smithy.model.shapes.ShortShape;
 import software.amazon.smithy.model.shapes.StringShape;
 import software.amazon.smithy.model.shapes.StructureShape;
 import software.amazon.smithy.model.shapes.UnionShape;
+import software.amazon.smithy.model.traits.DeprecatedTrait;
 import software.amazon.smithy.model.traits.DocumentationTrait;
 import software.amazon.smithy.model.traits.EnumDefinition;
 import software.amazon.smithy.model.traits.EnumTrait;
@@ -780,4 +781,110 @@ public class JsonSchemaConverterTest {
                 IoUtils.toUtf8String(getClass().getResourceAsStream("int-enums-disabled.jsonschema.v07.json")));
         Node.assertEquals(document.toNode(), expected);
     }
+
+    @Test
+    public void supportsDeprecatedTraitOnAStruct() {
+        StringShape string = StringShape.builder().id("smithy.api#String").build();
+        StructureShape shape = StructureShape.builder()
+                .id(ShapeId.from("a.b#C"))
+                .addMember(MemberShape.builder()
+                        .id(ShapeId.from("a.b#C$member"))
+                        .target(string.getId())
+                        .build())
+                .addTrait(DeprecatedTrait.builder()
+                        .message("I'm deprecated")
+                        .since("sinceVersion")
+                        .build())
+                .build();
+        Model model = Model.builder().addShapes(shape, string).build();
+        JsonSchemaConfig config = new JsonSchemaConfig();
+        config.setJsonSchemaVersion(JsonSchemaVersion.DRAFT2020_12);
+        SchemaDocument document = JsonSchemaConverter.builder()
+                .model(model)
+                .config(config)
+                .build()
+                .convertShape(shape);
+
+        assertThat(document.getRootSchema().isDeprecated(), equalTo(true));
+    }
+    
+    @Test
+    public void dontAddDeprecatedTraitOnAStructWhenOldVersion() {
+        StringShape string = StringShape.builder().id("smithy.api#String").build();
+        StructureShape shape = StructureShape.builder()
+                .id(ShapeId.from("a.b#C"))
+                .addMember(MemberShape.builder()
+                        .id(ShapeId.from("a.b#C$member"))
+                        .target(string.getId())
+                        .build())
+                .addTrait(DeprecatedTrait.builder()
+                        .message("I'm deprecated")
+                        .since("sinceVersion")
+                        .build())
+                .build();
+        Model model = Model.builder().addShapes(shape, string).build();
+        JsonSchemaConfig config = new JsonSchemaConfig();
+        config.setJsonSchemaVersion(JsonSchemaVersion.DRAFT07);
+        SchemaDocument document = JsonSchemaConverter.builder()
+                .model(model)
+                .config(config)
+                .build()
+                .convertShape(shape);
+
+        assertThat(document.getRootSchema().isDeprecated(), equalTo(false));
+    }
+
+    @Test
+    public void supportsDeprecatedTraitOnAMember() {
+        StringShape string = StringShape.builder().id("smithy.api#String").build();
+        StructureShape shape = StructureShape.builder()
+                .id(ShapeId.from("a.b#C"))
+                .addMember(MemberShape.builder()
+                        .id(ShapeId.from("a.b#C$member"))
+                        .target(string.getId())
+                        .addTrait(DeprecatedTrait.builder()
+                                .message("I'm deprecated")
+                                .since("sinceVersion")
+                                .build())
+                        .build())
+                .build();
+        Model model = Model.builder().addShapes(shape, string).build();
+        JsonSchemaConfig config = new JsonSchemaConfig();
+        config.setJsonSchemaVersion(JsonSchemaVersion.DRAFT2020_12);
+        SchemaDocument document = JsonSchemaConverter.builder()
+                .model(model)
+                .config(config)
+                .build()
+                .convertShape(shape);
+
+        Schema memberSchema = document.getRootSchema().getProperties().get("member");
+        assertThat(memberSchema.isDeprecated(), equalTo(true));
+    }
+
+    @Test
+    public void dontAddDeprecatedTraitOnAMemberWhenOldVersion() {
+        StringShape string = StringShape.builder().id("smithy.api#String").build();
+        StructureShape shape = StructureShape.builder()
+                .id(ShapeId.from("a.b#C"))
+                .addMember(MemberShape.builder()
+                        .id(ShapeId.from("a.b#C$member"))
+                        .target(string.getId())
+                        .addTrait(DeprecatedTrait.builder()
+                                .message("I'm deprecated")
+                                .since("sinceVersion")
+                                .build())
+                        .build())
+                .build();
+        Model model = Model.builder().addShapes(shape, string).build();
+        JsonSchemaConfig config = new JsonSchemaConfig();
+        config.setJsonSchemaVersion(JsonSchemaVersion.DRAFT07);
+        SchemaDocument document = JsonSchemaConverter.builder()
+                .model(model)
+                .config(config)
+                .build()
+                .convertShape(shape);
+
+        Schema memberSchema = document.getRootSchema().getProperties().get("member");
+        assertThat(memberSchema.isDeprecated(), equalTo(false));
+    }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/traits/DeprecatedTraitTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/traits/DeprecatedTraitTest.java
index bcb4c8979..242d310ce 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/traits/DeprecatedTraitTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/traits/DeprecatedTraitTest.java
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
 import software.amazon.smithy.model.SourceException;
 import software.amazon.smithy.model.node.Node;
 import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.shapes.ShapeType;
 
 public class DeprecatedTraitTest {
     @Test
@@ -48,4 +49,29 @@ public class DeprecatedTraitTest {
             provider.createTrait(ShapeId.from("smithy.api#deprecated"), ShapeId.from("ns.qux#foo"), Node.from("abc"));
         });
     }
+
+    @Test
+    public void returnDefaultDescription() {
+        DeprecatedTrait deprecatedTrait = DeprecatedTrait.builder().build();
+        assertThat(deprecatedTrait.getDeprecatedDescription(ShapeType.OPERATION), equalTo("This operation is deprecated."));
+        assertThat(deprecatedTrait.getDeprecatedDescription(ShapeType.STRING), equalTo("This shape is deprecated."));
+    }
+
+    @Test
+    public void returnDescriptionWhenMessageSet() {
+        DeprecatedTrait deprecatedTrait = DeprecatedTrait.builder().message("Use X shape instead.").build();
+        assertThat(deprecatedTrait.getDeprecatedDescription(ShapeType.STRING), equalTo("This shape is deprecated: Use X shape instead."));
+    }
+
+    @Test
+    public void returnDescriptionWhenSinceSet() {
+        DeprecatedTrait deprecatedTrait = DeprecatedTrait.builder().since("2020-01-01").build();
+        assertThat(deprecatedTrait.getDeprecatedDescription(ShapeType.STRING), equalTo("This shape is deprecated since 2020-01-01."));
+    }
+
+    @Test
+    public void returnDescriptionWhenBothSinceAndMessageSet() {
+        DeprecatedTrait deprecatedTrait = DeprecatedTrait.builder().since("2020-01-01").message("Use X shape instead.").build();
+        assertThat(deprecatedTrait.getDeprecatedDescription(ShapeType.STRING), equalTo("This shape is deprecated since 2020-01-01: Use X shape instead."));
+    }
 }
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 eb87b08b6..6a1318e4b 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
@@ -43,6 +43,7 @@ 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.DeprecatedTrait;
+import software.amazon.smithy.model.traits.DocumentationTrait;
 import software.amazon.smithy.model.traits.DynamicTrait;
 import software.amazon.smithy.model.traits.ExternalDocumentationTrait;
 import software.amazon.smithy.model.traits.SensitiveTrait;
@@ -131,6 +132,26 @@ public class OpenApiJsonSchemaMapperTest {
         assertThat(document.getRootSchema().getExtension("deprecated").get(), equalTo(Node.from(true)));
     }
 
+    @Test
+    public void appendsDeprecatedInfoInDescription() {
+        String message = "Use a.b#D instead.";
+        String since = "2020-01-01";
+        IntegerShape shape = IntegerShape.builder()
+                .id("a.b#C")
+                .addTrait(DeprecatedTrait.builder().message(message).since(since).build())
+                .addTrait(new DocumentationTrait("This is an integer."))
+                .build();
+        Model model = Model.builder().addShape(shape).build();
+        SchemaDocument document = JsonSchemaConverter.builder()
+                .addMapper(new OpenApiJsonSchemaMapper())
+                .model(model)
+                .build()
+                .convertShape(shape);
+
+        String expected = "This is an integer.\nThis shape is deprecated since 2020-01-01: Use a.b#D instead.";
+        assertThat(document.getRootSchema().getDescription().get(), equalTo(expected));
+    }
+
     @Test
     public void supportsInt32() {
         IntegerShape shape = IntegerShape.builder().id("a.b#C").build();