Skip to main content

marshalling

Maps Smithy protocol traits to generated SDK marshaller/unmarshaller code patterns

Source facts

Repository
aws/aws-sdk-net
Last source activity
August 12, 2026 at 16:26
Detected SKILL.md language
English
Stars
131
Forks
890

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
marshalling
description
Maps Smithy protocol traits to generated SDK marshaller/unmarshaller code patterns
# Skill: Marshalling ## File Layout Under `Generated/Model/Internal/MarshallTransformations/`: | File | Class | Base/Interface | |---|---|---| | `{Operation}RequestMarshaller.cs` | `IMarshaller<IRequest, {Operation}Request>` | — | | `{Operation}ResponseUnmarshaller.cs` | `JsonResponseUnmarshaller` (or protocol equivalent) | Dispatches errors | | `{Shape}Marshaller.cs` | `IRequestMarshaller<{Shape}, JsonMarshallerContext>` | Nested structs in request path | | `{Shape}Unmarshaller.cs` | `IJsonUnmarshaller<{Shape}, JsonUnmarshallerContext>` | Nested structs in response path | | `{Exception}Unmarshaller.cs` | `IJsonErrorResponseUnmarshaller<{Exception}, JsonUnmarshallerContext>` | — | Singleton patterns differ by file type: - Structure marshallers: `public readonly static {Shape}Marshaller Instance = new {Shape}Marshaller();` - Many operation marshallers/unmarshallers use `private static ... _instance = new ...();` plus a public `Instance` property (and sometimes `internal static GetInstance()`). ## Class Signature All marshallers and unmarshallers should be partial classes. ## Request Marshaller Scaffolding Every request marshaller sets up: ```csharp IRequest request = new DefaultRequest(publicRequest, "Amazon.{ServiceName}"); request.Headers["Content-Type"] = "{content-type}"; // protocol-dependent request.Headers[Amazon.Util.HeaderKeys.XAmzApiVersion] = "{version}"; // from ServiceShape.ApiVersion request.HttpMethod = "{method}"; // from @http trait request.ResourcePath = "{uri}"; // from @http trait, with labels interpolated ``` Then serializes members based on placement rules, then returns `request`. ## Member Placement | Smithy trait | Where | SDK pattern | |---|---|---| | `@httpQuery("name")` | Query string | `request.Parameters.Add("name", StringUtils.FromString(...))` | | `@httpLabel` | URI segment | Replace `{member}` in `request.ResourcePath` | | `@httpHeader("name")` | Header | `request.Headers["name"] = ...` | | `@httpPrefixHeaders("prefix")` | Multiple headers | Loop dict, prefix each key | | `@httpPayload` | Entire body | Direct stream/string (skips body serialization) | | `@httpResponseCode` | (response only) | `response.HttpStatusCode` | | No HTTP trait | Body | Protocol-specific serialization | For `awsJson1.x` and `query`/`ec2Query`: all members go in the body (no HTTP binding traits). ## Wire Name Resolution | Protocol | Rule | |---|---| | restJson1 / awsJson1.x | `@jsonName` if present, else Smithy member name (camelCase) | | restXml | `@xmlName` if present, else Smithy member name (camelCase) | | query / ec2Query | `@ec2QueryName` or PascalCase of member name | ## Request Body Serialization ### JSON (restJson1, awsJson1.x) For request marshallers if any of the members are marshalled in the body OR marked with @httpPayload you must setup the PooledContentStream like so: ```csharp #if !NETFRAMEWORK request.ContentStream = new PooledContentStream(); using Utf8JsonWriter writer = new Utf8JsonWriter(((PooledContentStream)request.ContentStream).BufferWriter); #else using var memoryStream = new MemoryStream(); using Utf8JsonWriter writer = new Utf8JsonWriter(memoryStream); #endif ``` ```csharp if (publicRequest.IsSetFoo()) { context.Writer.WritePropertyName("foo"); // wire name context.Writer.WriteStringValue(publicRequest.Foo); } ``` - Structures: `WriteStartObject` → `{Shape}Marshaller.Instance.Marshall(item, context)` → `WriteEndObject` - Lists: `WriteStartArray` → loop items → `WriteEndArray` - Maps: `WriteStartObject` → loop `WritePropertyName(key)` + write value → `WriteEndObject` - Required strings: throw `Amazon{ServiceName}Exception` if null/empty before serialization At the end of the request marshaller after flushing the writer write: ```csharp #if NETFRAMEWORK request.Content = memoryStream.ToArray(); #endif ``` For structure marshallers loop through the structures members and use the rules laid out in Type → Marshal/Unmarshal ### XML (restXml) ```csharp xmlWriter.WriteStartElement("MemberName"); // or @xmlName xmlWriter.WriteValue(publicRequest.Foo); xmlWriter.WriteEndElement(); ``` - `@xmlFlattened` lists omit the wrapper element - `@xmlAttribute` members become XML attributes on the parent element - `@xmlNamespace` adds `xmlns` attribute ### Query (query, ec2Query) ```csharp request.Parameters.Add("MemberName", StringUtils.FromString(publicRequest.Foo)); ``` - Lists: `MemberName.member.{N}` (query) or `MemberName.{N}` (ec2Query) - Maps: `MemberName.entry.{N}.key` / `MemberName.entry.{N}.value` ## Response Unmarshaller Body ### JSON ```csharp while (context.ReadAtDepth(targetDepth, ref reader)) { if (context.TestExpression("foo", targetDepth, ref reader)) { response.Foo = StringUnmarshaller.Instance.Unmarshall(context, ref reader); continue; } } ``` - Lists: `new JsonListUnmarshaller<T, TUnmarshaller>(TUnmarshaller.Instance)` - Maps: `new JsonDictionaryUnmarshaller<K, V, KU, VU>(...)` ### XML ```csharp while (context.Read()) { if (context.TestExpression("MemberName", targetDepth)) { response.Foo = StringUnmarshaller.Instance.Unmarshall(context); continue; } } ``` - Flattened lists: test on element name directly - Non-flattened lists: test on `ListName/member` ## Type → Marshal/Unmarshal | .NET type | JSON Marshal | JSON Unmarshal | |---|---|---| | `string` | `WriteStringValue` | `StringUnmarshaller` | | `int?` | `WriteNumberValue` | `IntUnmarshaller` | | `long?` | `WriteNumberValue` | `LongUnmarshaller` | | `bool?` | `WriteBooleanValue` | `BoolUnmarshaller` | | `float?` | `WriteNumberValue` | `FloatUnmarshaller` | | `double?` | `WriteNumberValue` | `DoubleUnmarshaller` | | `DateTime?` | Format-dependent (see below) | `DateTimeUnmarshaller` | | `MemoryStream` | `WriteStringValue(Convert.ToBase64String(...))` | `MemoryStreamUnmarshaller` | | `List<T>` | Array loop | `JsonListUnmarshaller<ElementType, ElementUnmarshaller>` | | `Dictionary<K,V>` | Object loop | `JsonDictionaryUnmarshaller<K, V, KeyUnmarshaller, ValueUnmarshaller>` | | Structure | `{Shape}Marshaller.Instance` | `{Shape}Unmarshaller.Instance` | ### Timestamp Formats An explicit `@timestampFormat` (on the member or its target) always wins. When unset, the default is **binding-specific**, not one per protocol — see the binding-default table below. | `@timestampFormat` | Marshal (body) | Marshal (header/query/label) | |---|---|---| | `date-time` | `WriteStringValue(StringUtils.FromDateTimeToISO8601WithOptionalMs(value))` | `StringUtils.FromDateTimeToISO8601WithOptionalMs(value)` | | `http-date` | `WriteStringValue(StringUtils.FromDateTimeToRFC822(value))` | `StringUtils.FromDateTimeToRFC822(value)` | | `epoch-seconds` | `WriteNumberValue(System.Convert.ToInt64(StringUtils.FromDateTimeToUnixTimestamp(value.Value)))` | `StringUtils.FromDateTimeToUnixTimestamp(value)` | restJson1 binding defaults when `@timestampFormat` is unset (matches the C2J generator's output): | Binding | Default | |---|---| | Body / structure member | `epoch-seconds` (restJson1's document-timestamp default per the Smithy spec; the generic `@timestampFormat` default of `date-time` applies only when a protocol sets none) | | `@httpHeader` | `http-date` | | `@httpQuery`, `@httpLabel` | `date-time` | String forms pass the nullable `DateTime?` straight to the `StringUtils` overload; the epoch form unwraps with `.Value`. ## Error Dispatch In `{Operation}ResponseUnmarshaller.UnmarshallException`: ```csharp if (errorResponse.Code != null && errorResponse.Code.Equals("{smithyShapeName}")) return {Exception}Unmarshaller.Instance.Unmarshall(contextCopy, errorResponse, ref readerCopy); ``` Error code = Smithy shape name (e.g. `"ChannelNotFound"`), not the .NET exception name. Fallback: `new Amazon{Service}Exception(errorResponse.Message, ...)`. ### Exception Unmarshaller ```csharp public {Exception} Unmarshall(JsonUnmarshallerContext context, ErrorResponse errorResponse, ref StreamingUtf8JsonReader reader) { if (context.Stream.Length > 0) context.Read(ref reader); var unmarshalledObject = new {Exception}(errorResponse.Message, errorResponse.InnerException, errorResponse.Type, errorResponse.Code, errorResponse.RequestId, errorResponse.StatusCode); int targetDepth = context.CurrentDepth; if (context.Stream.Length > 0) { while (context.ReadAtDepth(targetDepth, ref reader)) { // Additional exception members deserialized here (if any beyond "message") } } return unmarshalledObject; } ``` ## Protocol Differences | Concern | restJson1 | awsJson1.x | restXml | query | ec2Query | |---|---|---|---|---|---| | Content-Type | `application/json` | `application/x-amz-json-1.{0,1}` | (none/xml) | `application/x-www-form-urlencoded` | `application/x-www-form-urlencoded` | | Routing | HTTP method + path | `X-Amz-Target: {ServiceName}.{Operation}` | HTTP method + path | `Action={Operation}` param | `Action={Operation}` param | | Member placement | HTTP traits | All body | HTTP traits | All body | All body | | Body format | JSON | JSON | XML | URL-encoded | URL-encoded | | Timestamp body default | `epoch-seconds` | `epoch-seconds` | `date-time` | `date-time` | `date-time` | | Error code source | JSON `code` or `__type` | JSON `code` or `__type` | XML `<Code>` | XML `<Code>` | XML `<Code>` | | Error wrapping | None | None | `<ErrorResponse><Error>` | `<ErrorResponse><Error>` | `<Response><Errors><Error>` | | Response unmarshaller base | `JsonResponseUnmarshaller` | `JsonResponseUnmarshaller` | `XmlResponseUnmarshaller` | `XmlResponseUnmarshaller` | `XmlResponseUnmarshaller` | | Request uses `UseQueryString` | Yes (for `@httpQuery`) | No | Yes (for `@httpQuery`) | No | No | ## Adding a New Protocol When implementing the next protocol, update this skill with: 1. Any new marshal/unmarshal patterns not covered above 2. Protocol-specific base classes or interfaces 3. Edge cases discovered during implementation
View on GitHub