--- name: marshalling description: What the SmithyDotNet generator must emit for request marshallers and response/error unmarshallers, per Smithy binding trait and protocol. Use when writing or reviewing any marshaller/unmarshaller writer. --- # Skill: Marshalling This skill states the emitted output. Where a rule exists only for parity with the legacy C2J generator, it says so; everything else follows from the Smithy spec. ## File Layout Under `Generated/Model/Internal/MarshallTransformations/`, all `partial`: | File | Class | Notes | |---|---|---| | `{Operation}RequestMarshaller.cs` | `IMarshaller` | | | `{Operation}ResponseUnmarshaller.cs` | `JsonResponseUnmarshaller` (or protocol equivalent) | Dispatches errors | | `{Shape}Marshaller.cs` | `IRequestMarshaller<{Shape}, JsonMarshallerContext>` (or protocol equivalent) | Nested structures in the request path | | `{Shape}Unmarshaller.cs` | `IJsonUnmarshaller<{Shape}, JsonUnmarshallerContext>` (or protocol equivalent) | Nested structures in the response path | | `{Exception}Unmarshaller.cs` | `IJsonErrorResponseUnmarshaller<{Exception}, JsonUnmarshallerContext>` (or protocol equivalent) | | | `{Operation}EndpointDiscoveryMarshaller.cs` | `IMarshaller` | Endpoint discovery only; see `sdk-conventions` | An operation output already named `{Operation}Response` that a member also targets is typed with the `{Operation}Response` class itself (no separate model class); its structure unmarshaller is `{Shape}StructureUnmarshaller`, since `{Shape}Unmarshaller` is the operation's response unmarshaller. No C2J service has this shape, so there is no parity target. Structure (un)marshaller class names come only from `GenerationContext.StructureMarshallerName` / `StructureUnmarshallerName`. `TypeDescriptor.MarshallerName` / `UnmarshallerName` carry them for a structure target; writers read those (or the context, when declaring the class or naming the file) and never compose `{Shape}Marshaller` / `{Shape}Unmarshaller` themselves, so a naming rule needs no per-protocol code. Structure marshallers expose `public readonly static {Shape}Marshaller Instance = new {Shape}Marshaller();`. Operation marshallers/unmarshallers expose a `private static` instance behind a public `Instance` property. ## Request Marshaller Scaffolding Every request marshaller emits, in this order: 1. `new DefaultRequest(publicRequest, "Amazon.{ServiceName}")`, then `@requestCompression` (below). 2. `Content-Type` (protocol-dependent, or the `overrideContentType` customization when set — below), omitted for GET/DELETE and for operations with no body. 3. `HeaderKeys.XAmzApiVersion` (the service shape's version) and `HttpMethod`. 4. `@httpQuery` members, then `@httpQueryParams`; `@httpPrefixHeaders`, then `@httpHeader` members. 5. `@httpLabel` members as `AddPathResource` calls, then `request.ResourcePath` set to the `@http` uri template (labels left as `{name}` for the runtime to substitute). 6. The body (below), `@httpChecksumRequired`, `@unsignedPayload`, `UseQueryString`, `@endpoint` host prefix, `return request`. An operation that requires HTTP/2 pins `request.HttpProtocolVersion = System.Net.HttpVersion.Version20` (under `#if NET8_0_OR_GREATER`) right after the `DefaultRequest`. Which operations require it comes from the protocol trait's version lists, where `eventStreamHttp` defaults to `http` when absent or empty: | `http` | `eventStreamHttp` | Operations pinned to h2 | |---|---|---| | no `h2` | any | none | | `h2` without `http/1.1` | any | all | | `h2` and `http/1.1` | without `http/1.1` | those with an output event stream | | `h2` and `http/1.1` | with `http/1.1` | those with both an input and an output event stream | ### `overrideContentType` customization The service-level `overrideContentType` customization (a top-level string in `*.customizations.json`, read into `GenerationContext.Customizations.OverrideContentType`) hard-codes the request `Content-Type`: `request.Headers["Content-Type"] = "{value}";`. On restJson1 it replaces the per-operation value (including the blob-payload default) but is still skipped for input event streams and GET/DELETE; it's emitted even for body-less operations. On awsJson it replaces only the versioned Content-Type; `X-Amz-Target` is unchanged. A handful of restJson1 services use it to send `application/x-amz-json-1.1` instead of the default `application/json` (finspace, finspace-data, lex.v2), matching C2J. ## Member Placement | Smithy trait | Where | SDK pattern | |---|---|---| | `@httpQuery("name")` scalar | Query string | `request.Parameters.Add("name", StringUtils.FromString(...))` | | `@httpQuery("name")` `list` | Query string | `request.ParameterCollection.Add("name", publicRequest.Prop)` (repeated params, ordinal-sorted at runtime) | | `@httpQuery("name")` `list` | Query string | `request.ParameterCollection.Add("name", publicRequest.Prop.ConvertAll(item => StringUtils.FromX(item)))` | | `@httpQueryParams` map | Query string | Loop entries into query params (see below); `@httpQuery` wins on key collision | | `@httpLabel` | URI segment | `if (!publicRequest.IsSetProp()) throw new Amazon{BaseName}Exception(...)`, then `request.AddPathResource("{name}", )`. A greedy label keeps `{name+}` as the key and passes the value through `.TrimStart('/')` | | `@httpHeader("name")` scalar | Header | `request.Headers["name"] = ...` | | `@httpHeader("name")` `@mediaType` string | Header | Base64: `Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(...))`; the read side decodes (C2J "jsonvalue"). Body-bound `@mediaType` strings are plain | | `@httpHeader("name")` `list` | Header | `request.Headers["name"] = StringUtils.FromList(publicRequest.Prop)` (comma join, RFC-7230 quoting) | | `@httpHeader("name")` `list` | Header | `request.Headers["name"] = StringUtils.FromValueTypeList(publicRequest.Prop)` (the `List` overload lowercases bool and forces `DateTime` to RFC822, so no per-element or per-format branch is emitted) | | `@httpPrefixHeaders("prefix")` map | Multiple headers | Loop `map`, emit `{prefix}{key}` headers (see below); request & response | | `@httpPayload` | Entire body | Direct stream/string (skips body serialization) | | `@hostLabel` | Endpoint host prefix | `request.HostPrefix` label **in addition to** the member's normal binding (see below) | | `@httpResponseCode` | (response only) | `unmarshalledObject.{Prop} = (int)context.ResponseData.StatusCode;` (see below) | | No HTTP trait | Body | Protocol-specific serialization | Every query/header/label member is `IsSet`-guarded. A `@required` query member (an idempotency token excepted) first throws `Amazon{BaseName}Exception("Request object does not have required field {Prop} set")` when unset: `string.IsNullOrEmpty` for a string/enum, `== null` for anything else. Body members get no required check. List query/header bindings accept `list`/`list` (an enum element is a plain `string`, see type-mapping) and value-type element lists (int, long, bool, double, float, timestamp, intEnum), matching C2J. A **non-scalar** element (blob, structure, nested list/map) fails loud in both positions, as does a `@sparse` value-type list (a sparse string/enum list is accepted). A header `list` is only supported with the default/`http-date` format (the runtime helper always emits RFC822). A single member may carry `@httpQueryParams`: a `map` or `map>` whose entries each become query params under the `@httpQuery` rules (a `list` value repeats the key via `request.ParameterCollection`; a `map` adds `StringUtils.FromString(kvp.Value)` via `request.Parameters`). It's emitted **after** the explicit `@httpQuery` members, each entry guarded by a `ContainsKey` check that skips keys already set, so an explicit `@httpQuery` wins the collision per the Smithy precedence rule. The guard intentionally does NOT cover query literals in the `@http` uri (those live in `request.SubResources`), matching C2J so migration doesn't change wire behavior. The map is request-only and never enters the body; its presence sets `request.UseQueryString = true`. A single member may carry `@httpPrefixHeaders`: a `map` whose entries each become a header `{prefix}{key}` (an `IsSet` guard, then a `foreach` assigning `request.Headers[$"{prefix}{kvp.Key}"] = kvp.Value;`). It's emitted **before** the explicit `@httpHeader` members, so a colliding name (only possible with an empty prefix) is overwritten by the later `@httpHeader` assignment, per the Smithy precedence rule. Valid on request, response **and error** structures. The map value must be a string (anything else fails loud). ## Wire Name Resolution `@jsonName` if present, else the Smithy member name verbatim. awsJson1.x ignores `@jsonName` (see below). ## Request Body Serialization ### JSON (restJson1, awsJson1.x) When any member is body-bound, or the `@httpPayload` is a structure or document, the marshaller writes a `Utf8JsonWriter` body over a `PooledContentStream` (`#if NETFRAMEWORK`: a `MemoryStream` copied to `request.Content`). Each body member is `IsSet`-guarded, then written per Type → Marshal/Unmarshal under its wire name. - Structures dispatch to `{Shape}Marshaller.Instance`; lists/maps loop and recurse to any depth. Map keys are always strings. - Collection leaves: string, value-type scalar, structure, document, or non-streaming blob. Non-sparse value-type leaves are non-nullable (see type-mapping), so they write bare: no `.Value` unwrap and no float/double NaN guard (both are member-only). `@timestampFormat` is honored on leaves. - `@sparse` leaves are nullable and JSON nulls are written, matching C2J. A sparse list null-guards its value-type and blob elements (a null string/structure/document already serializes as null); a sparse map null-guards every value kind. - Float/double **members** branch through `StringUtils.IsSpecial{Float,Double}Value` so NaN/±Infinity serialize as strings. Structure marshallers loop the structure's own members with the same rules. A request event-stream **event** is the one exception: the publisher marshaller calls `{Event}Marshaller` with the JSON object already open, then reads `context.Request.EventHeaders` and `context.Request.Content`. Event members route by trait: - `@eventHeader` → `IsSet`-guarded `EventStreamHeader("{memberName}")` (never `@jsonName`) with the typed setter for the target shape (`SetString` string/enum, `SetBool`/`SetInt32`/`SetInt64`/`SetTimestamp` with `.Value`, `SetByteBuf(….ToArray())` blob), added to `EventHeaders`. Any other target fails loud. - `@eventPayload` → `IsSet`-guarded: blob `Request.Content = ….ToArray()`, string/enum `Encoding.UTF8.GetBytes(…)` (publisher sends octet-stream / text/plain, C2J parity); structure/union `{Type}Marshaller.Instance.Marshall(…, context)` into the open object (the protocol tests expect the bare structure as the payload, not one wrapped under the member name). List/map fail loud. - Everything else is an ordinary body member (the implicit payload). ### `@httpPayload` (request) A `@httpPayload` member IS the entire body: no wrapping object/property name, no other member in the body (Smithy: all others are header/query/label). No `IsSet` guard (matches C2J), except string/enum. - **String** (enum too) → `text/plain` (or the target's `@mediaType` value when present), no writer scaffold: `if (publicRequest.IsSet{Prop}()) { request.Content = System.Text.Encoding.UTF8.GetBytes(publicRequest.{Prop}); }`. The guard diverges from C2J, whose unguarded `GetBytes` throws on an unset optional payload (DOTNET-8852). - **Structure** (a union too) → `application/json`; the scaffold, then the target's marshaller as the body object (`WriteStartObject` → `{Type}Marshaller.Instance.Marshall(publicRequest.{Prop}, context)` → `WriteEndObject`). - **Document** → `application/json`; the scaffold, then `Amazon.Runtime.Documents.Internal.Transform.DocumentMarshaller.Instance.Write(writer, publicRequest.{Prop});` with NO `WriteStartObject`/`WriteEndObject` wrapping (the document is the whole JSON value). No C2J precedent. - **Blob** (`MemoryStream`, or `Stream` when `@streaming`) → `application/octet-stream` (or the target's `@mediaType` value; overrides the top `application/json`). Always assigns `request.ContentStream = publicRequest.{Prop} ?? new MemoryStream();` first and ends with the Content-Type override, except when the input also has an `@httpHeader("Content-Type")` member: that header is emitted before the blob block and must win, so the blob's type moves to the top `Content-Type` line and the trailing override is dropped (restJson1 `TestPayloadBlob`). The Content-Length handling in between branches on the operation's `aws.auth#unsignedPayload` and the blob's `@requiresLength` (C2J parity): - **`@streaming` + `@unsignedPayload`, no `@requiresLength`** → seek to start and set Content-Length when the stream is seekable, else `Transfer-Encoding: chunked`. - **`@streaming` + `@requiresLength`** → the stream MUST be seekable (throws `InvalidOperationException` otherwise), then always sets Content-Length. `@requiresLength` wins over the unsigned chunked path. - **otherwise** → seek when seekable, always set Content-Length (no chunked). Separately, `aws.auth#unsignedPayload` on the operation emits `request.DisablePayloadSigning = true;` after the body block, for **any** body kind. List and map payloads fail loud. ### `@requestCompression` and `@httpChecksumRequired` (request) Both are one emitted call to a runtime helper in `Amazon.Runtime.Internal.Util`, at opposite ends of the body. - `@requestCompression` → `CompressionAlgorithmUtils.SetCompressionAlgorithm(request, CompressionEncodingAlgorithm.{encoding});` right after `new DefaultRequest(...)`. `encodings` is a preference list: the first **supported** entry wins and unsupported ones are skipped (`["br", "gzip"]` emits gzip); `gzip` is the whole supported set (`CompressionEncodingAlgorithm` has only `NONE` and `gzip`). When nothing in the list is supported we throw, where C2J warns and emits no call: silently dropping compression changes wire behavior with no signal. A `@streaming` `@requiresLength` payload is rejected (Smithy forbids it; the compressed length isn't known until the stream has been read). - `@httpChecksumRequired` → `ChecksumUtils.SetChecksumData(request);` **after** body serialization (the checksum covers the body), before the `DisablePayloadSigning` line. This is the legacy MD5-only trait. C2J also treats the flexible `aws.protocols#httpChecksum` as MD5-required; we deliberately don't (that trait is rejected as unsupported), so flexible-checksum operations stay off the MD5 path. ### `@endpoint` host prefix (request) An operation's `@endpoint` trait sets `request.HostPrefix` (the resolver prepends it to the endpoint host). Emitted last, after `UseQueryString`, before `return`. - **Static** (no labels) → `request.HostPrefix = $"data.";` - **Labeled** → each `@hostLabel` member is captured into an anonymous `hostPrefixLabels` object (field = modeled member name, value = `StringUtils.FromString(publicRequest.{Prop})`), validated with `HostPrefixUtils.IsValidLabelValue` (throws `Amazon{Service}Exception` naming the label and the 1–63 alphanumeric/dash rule), then interpolated into the prefix (`{name}` → `{hostPrefixLabels.name}`). ## Response Unmarshaller Body The body loop is `while (context.ReadAtDepth(targetDepth, ref reader))` with a `context.TestExpression("{wireName}", targetDepth, ref reader)` guard per member assigning `unmarshalledObject.{Prop} = {Unmarshaller}.Instance.Unmarshall(context, ref reader)` then `continue`. A response whose members are all headers (or empty) emits no reader loop at all. - Lists: `new JsonListUnmarshaller(TUnmarshaller.Instance)` - Maps: `new JsonDictionaryUnmarshaller(StringUnmarshaller.Instance, VU.Instance)` (key is always `string`/`StringUnmarshaller`). - Scalars per Type → Marshal/Unmarshal. Body timestamps are read format-agnostically, so epoch and date-time collections unmarshal identically. - Nested collections compose recursively: a map-of-list is `JsonDictionaryUnmarshaller, StringUnmarshaller, JsonListUnmarshaller>(...)`. An enum leaf (and an enum key) is `string`/`StringUnmarshaller`, never a ConstantClass generic argument; a non-streaming blob leaf is `MemoryStream`/`MemoryStreamUnmarshaller`. ### `@httpPayload` (response) A `@httpPayload` output member IS the whole body (replaces the named-field loop; other members are header-bound), into `unmarshalledObject`: - **String** → `using (var sr = new StreamReader(context.Stream)) { unmarshalledObject.Body = sr.ReadToEnd(); }` - **Structure** → reader + `if (reader.Reader.IsFinalBlock) return unmarshalledObject;` + `{Type}Unmarshaller.Instance.Unmarshall(context, ref reader)`. - **Document** → the structure scaffold with `Amazon.Runtime.Documents.Internal.Transform.DocumentUnmarshaller.Instance`. C2J models a document as a structure flagged `document: true`, so its output takes the structure-payload branch; we emit the same (bedrock-agentcore `GetAgentCardResponse`). - **Blob** (non-streaming, `MemoryStream`) → `Amazon.Util.AWSSDKUtils.CopyStream(context.Stream, ms)` into a new `MemoryStream`; assigned only when `ms.Length > 0`, so an empty body leaves the property null (matches C2J). - **Streaming blob** (`@streaming`, `Stream`) → assigns the raw `context.Stream` unbuffered and the unmarshaller class overrides `public override bool HasStreamingProperty => true` (matches C2J, see Polly `SynthesizeSpeech`). `@httpHeader` members read from `context.ResponseData` after. An `@httpPayload` **error** member fails loud: C2J never bound an error body to a payload, and emitting a never-populated property would be worse. ### `@httpResponseCode` (response) An `@httpResponseCode` output member (an integer) is populated from the HTTP status code itself, `unmarshalledObject.{Prop} = (int)context.ResponseData.StatusCode;`, **not** read from the body or a header. Matches C2J. On an error the trait "is simply ignored" (Smithy spec) and the member falls through to the body like any ordinary member. ### Event streams (response) An output member targeting a `@streaming` union/structure is an event stream. It IS the body: the unmarshaller emits `unmarshalledObject.{Prop} = new {UnionClass}(context.Stream);` instead of a JSON reader loop (C2J parity), and the class overrides `HasStreamingProperty => true` and `ShouldReadEntireResponse(...) => false` so Core never buffers the body. The `{UnionClass}` itself, and everything else event streams emit, is in sdk-conventions → Event Streams. Each non-error union member targets an **event structure** that gets its own `{Event}Unmarshaller`, invoked per event from the event-stream class's `EventMapping`. A member carries at most one of `@eventPayload`/`@eventHeader`: - **`@eventPayload`** (≤1 per event) → the raw message payload, no JSON body loop for it. Blob → `unmarshalledObject.{Prop} = context.Stream as MemoryStream;`; string → `using (var sr = new StreamReader(context.Stream)) { unmarshalledObject.{Prop} = sr.ReadToEnd(); }`; structure → `unmarshalledObject.{Prop} = {Type}Unmarshaller.Instance.Unmarshall(context, ref reader);`. The SEP requires all other members to carry `@eventHeader` when a payload member exists. - **`@eventHeader`** → read from the event-message header via `context.ResponseData`, guarded by `IsEventHeaderPresent("{ModeledName}")` (the wire header key is the member name verbatim). The accessor on `GetEventStreamHeader("{ModeledName}")` follows the target shape: string/enum → `AsString()`, boolean → `AsBool()`, integer/intEnum → `AsInt32()`, long → `AsInt64()`, timestamp → `AsTimestamp()`, blob → `new MemoryStream(...AsByteBuf())`. Any other target fails loud. - **unbound** → the JSON body, read through the ordinary reader loop. An event can carry `@eventHeader` members **without** an `@eventPayload` member: a headers-only event (only header reads, no reader loop) or an implicit-payload event (headers from headers, the rest from the JSON body). This is **not** an all-or-nothing branch (C2J gets this wrong; the Smithy protocol tests `HeadersEvent`/`HeadersAndImplicitPayloadEvent` cover it). The reader loop is skipped **only** for events with an explicit payload or headers alone; an ordinary structure with no members still runs it, so the `{}` tokens are consumed and the parent's remaining members are not misread. ### Event streams (request) A request event stream adds a publisher marshaller on top of the marker interface and per-event partials described in sdk-conventions. The union gets no plain model class or structure marshaller, but its event members still get marshallers. - `{Union}PublisherMarshaller`: `NextEventAsync` pulls the consumer's next event, dispatches on `evnt is {Event}`, marshals it with `{Event}Marshaller.Instance`, and sets `eventType` to the union member name verbatim (the wire `:event-type`, not the shape name). The wire `:content-type` follows the event's `@eventPayload` member: a blob payload → `application/octet-stream`, a string payload → `text/plain`, both read from `context.Request.Content`; a structure payload or an implicit body → `application/json` from the JSON writer's stream. (C2J emits `text/plain` for a structure payload too, but the event marshaller writes it as JSON, so JSON is kept here.) CBOR is not handled: restJson1 only. - The request member becomes a `Func> {Member}Publisher` property (keeps any modeled `[AWSProperty]`/`[Obsolete]`, no `IsSet`). The request marshaller wires it: `request.EventStreamPublisher = new {Union}PublisherMarshaller(publicRequest.{Member}Publisher)` with `Content-Type: application/vnd.amazon.eventstream`, replacing body serialization. ## Response Header Unmarshalling Output and error members bound with `@httpHeader` are read from the HTTP response headers via `context.ResponseData`, **not** the body reader; they're extracted after the reader loop, into `unmarshalledObject` on both the response and the exception path. Each member is guarded by `context.ResponseData.IsHeaderPresent("x-foo")` and assigned a ``: | Member target | `` (with `value` = `context.ResponseData.GetHeaderValue("x-foo")`) | |---|---| | string / enum | `value` (direct; enum rides the string path via implicit ConstantClass conversion) | | boolean | `bool.Parse(value)` (no culture: its two literals are culture-invariant) | | integer / intEnum | `int.Parse(value, CultureInfo.InvariantCulture)` | | long | `long.Parse(value, CultureInfo.InvariantCulture)` | | float | `float.Parse(value, CultureInfo.InvariantCulture)` | | double | `double.Parse(value, CultureInfo.InvariantCulture)` | | timestamp, date-time / http-date | `DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal \| DateTimeStyles.AdjustToUniversal)` | | timestamp, epoch-seconds | `Amazon.Util.AWSSDKUtils.ConvertFromUnixEpochSeconds(int.Parse(value, CultureInfo.InvariantCulture))` | | `list` / `list` | `MultiValueHeaderParser.ToStringList(value)` | | `list` (int/long/bool/double/float) | `MultiValueHeaderParser.ToValueTypeList(value)` (T = the non-nullable element type, e.g. `int`) | | `list` | `MultiValueHeaderParser.ToDateTimeList(value, "FMT")`, where `FMT` is the runtime's format name (`RFC822`/`ISO8601`/`UnixTimestamp`, header default `RFC822`), **not** the Smithy token | A non-scalar list element fails loud, as does a `@sparse` value-type element. `MultiValueHeaderParser` lives in `Amazon.Runtime.Internal.Util`; `CultureInfo`/`DateTimeStyles` come from `System.Globalization`. Both namespaces are imported unconditionally. ### `@httpPrefixHeaders` (response / error) A `map` member bound with `@httpPrefixHeaders` collects every response header whose name starts with the prefix into a local dictionary named `headersFor{Property}` (matching C2J), stripping the prefix from each key. An empty prefix collects all headers. Assigned only when non-empty (matches C2J). Same output on the response and exception unmarshallers. ## Type → Marshal/Unmarshal | Smithy target | .NET | JSON Marshal | JSON Unmarshal | |---|---|---|---| | string / enum | `string` / ConstantClass | `WriteStringValue` | `StringUnmarshaller` | | integer / intEnum | `int?` | `WriteNumberValue` | `IntUnmarshaller` | | long | `long?` | `WriteNumberValue` | `LongUnmarshaller` | | boolean | `bool?` | `WriteBooleanValue` | `BoolUnmarshaller` | | float | `float?` | `WriteNumberValue` (NaN/∞ as strings, members only) | `FloatUnmarshaller` | | double | `double?` | `WriteNumberValue` (NaN/∞ as strings, members only) | `DoubleUnmarshaller` | | timestamp | `DateTime?` | Format-dependent (see below) | `DateTimeUnmarshaller` | | blob | `MemoryStream` | `StringUtils.WriteBase64StringValue(context.Writer, ...)` | `MemoryStreamUnmarshaller` | | document | `Amazon.Runtime.Documents.Document` | `DocumentMarshaller.Instance.Write` | `DocumentUnmarshaller.Instance` | | list | `List` | Array loop | `JsonListUnmarshaller` | | map | `Dictionary` | Object loop | `JsonDictionaryUnmarshaller` | | structure / union | Generated class | `{Shape}Marshaller.Instance` | `{Shape}Unmarshaller.Instance` | A value type that is nullable in its position (a member, or a `@sparse` element; see type-mapping → Nullability Rules) unwraps with `.Value` before the write and takes the `Nullable*Unmarshaller`; a non-sparse element writes bare and takes the plain unmarshaller. ### Timestamp Formats An explicit `@timestampFormat` (on the member or its target) always wins. When unset, the default is **per binding**, not one per protocol. | `@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(Amazon.Util.AWSSDKUtils.ConvertToUnixEpochSecondsDecimal(value.Value))` | `StringUtils.FromDateTimeToUnixTimestamp(value)` | restJson1 defaults when `@timestampFormat` is unset (matches C2J's output). They apply when marshalling and when reading headers; body reads auto-detect the wire format. | Binding | Default | |---|---| | Body / structure member | `epoch-seconds` (restJson1's document-timestamp default per the Smithy spec) | | `@httpHeader` | `http-date` | | `@httpQuery`, `@httpLabel` | `date-time` | String forms pass the nullable `DateTime?` straight to the `StringUtils` overload; the epoch form unwraps with `.Value`. `epoch-seconds` in a **body** is a JSON number that may carry a fraction, so it goes through `ConvertToUnixEpochSecondsDecimal` (millisecond precision, `decimal` for identical digits on every TFM), not the whole-second `StringUtils.FromDateTimeToUnixTimestamp`. Header/query/label positions are whole seconds, matching C2J. ## Data Type Swaps A `dataTypeSwap` member (see type-mapping → Data Type Swaps) carries `MarshallerOverride` / `UnmarshallerOverride` on its `TypeDescriptor`. Only the shared scalar helpers honor them, so a new protocol writer gets swaps by reusing those helpers; an omitted override keeps the modeled conversion: - JSON body (`JsonScalarMarshaller.WriteScalar`): `context.Writer.WriteStringValue({Marshaller}(x))`, or `WriteNumberValue` when the marshaller is `Amazon.Util.AWSSDKUtils.ConvertToUnixEpochMilliseconds` (C2J keys on the name, assuming every other returns a string). `.Value` is unwrapped first when the modeled type is a value type and the swapped type is nullable. - Query / header (`JsonRequestMarshallerWriter.StringConversion`, and the header branch that assigns strings directly): `{Marshaller}(publicRequest.X)`, passed the property as-is (no `.Value`). A required swapped query member is checked with `== null`, since its type may not be a string. - Unmarshal (`JsonBodyMemberUnmarshaller.ScalarUnmarshaller`): `var unmarshaller = {Unmarshaller}.Instance;`, emitted verbatim. - A response header swap that names an `Unmarshaller` fails loud (not supported yet). ## Error Dispatch In `{Operation}ResponseUnmarshaller.UnmarshallException`, each error is matched with `errorResponse.Code != null && errorResponse.Code.Equals("{smithyShapeName}")` (the Smithy shape name, not the .NET exception name) and dispatched to `{Exception}Unmarshaller.Instance.Unmarshall(contextCopy, errorResponse, ref readerCopy)`. Fallback: `new Amazon{Service}Exception(errorResponse.Message, ...)`. ### Exception Unmarshaller `Unmarshall(JsonUnmarshallerContext context, ErrorResponse errorResponse, ref StreamingUtf8JsonReader reader)` constructs the exception from the six `errorResponse` fields (message, inner exception, type, code, request id, status code). When the error has body-bound members beyond `message`, a single `if (context.Stream.Length > 0)` wraps the first-token read and the body loop, so an empty error body skips both. `@httpHeader` members are then read from `context.ResponseData` (see Response Header Unmarshalling). ## awsJson1.0 / awsJson1.1 Same JSON body (un)marshalling as restJson1. Every operation is `POST /` with `X-Amz-Target: {ServiceShapeName}.{OperationName}` (the service's shape name, not its sdkId) and `Content-Type: application/x-amz-json-1.0|1.1`; an operation with no input members sends `{}`. HTTP binding traits and `@jsonName` are ignored when a model carries them. Spec: [protocol behaviors](https://smithy.io/2.0/aws/protocols/aws-json-1_1-protocol.html#protocol-behaviors). 1.0 and 1.1 differ only in the Content-Type version and the `__type` form of errors ([differences](https://smithy.io/2.0/aws/protocols/aws-json-1_0-protocol.html#differences-between-awsjson1-0-and-awsjson1-1)); Core handles the `__type` difference transparently, so the generator only varies the version string. Every one of those decisions is gated on `GenerationContext.UsesHttpBindings`; `AwsJsonCodegenTests` pins the emitted code and the JSONRPC10/JsonProtocol protocol tests verify it end to end. ## rpcv2Cbor Spec: [Smithy RPC v2 CBOR](https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2-cbor.html). Every operation is `POST service/{ServiceShapeName}/operation/{OperationName}` with `smithy-protocol: rpc-v2-cbor`, `Accept: application/cbor`, and a CBOR map body with `Content-Type: application/cbor` unless the input is `Unit`. HTTP binding traits, `@jsonName` and `@timestampFormat` are ignored: keys are member names and timestamps are tag 1. The output is wire-compatible with C2J's CBOR templates and formatted like the JSON writers' output; the `Cbor*` writers mirror the JSON ones and are pinned by `RpcV2CborCodegenTests`. A `@sparse` element writes CBOR null per the spec; C2J writes `{}` for a null structure element in a sparse list, but no CBOR service model uses `@sparse`. Documents and streaming blobs are rejected by `UnsupportedTraitValidator`; event streams are not supported. ## awsQueryCompatible Service trait for awsJson1.0 and rpcv2Cbor services that moved off awsQuery ([spec](https://smithy.io/2.0/aws/protocols/aws-query-protocol.html#aws-protocols-awsquerycompatible-trait)): the request announces query mode and errors still dispatch on the shape name, not the rewritten query error code. ## Other Protocols (not yet implemented) restXml, awsQuery and ec2Query: the target output is defined by the C2J templates (`generator/ServiceClientGeneratorLib/Generators/Marshallers/*.tt`). awsQuery/ec2Query route via an `Action={Operation}` param with URL-encoded bodies; restXml keeps the HTTP binding traits with an XML body (`@xmlName`/`@xmlFlattened`/`@xmlAttribute`/`@xmlNamespace`) and, per the Smithy spec, defaults body timestamps to `date-time`. The XML-response family uses `XmlResponseUnmarshaller`. Wire names: restXml `@xmlName`, awsQuery the member name verbatim, ec2Query `@ec2QueryName` or the member name with its first letter upper-cased. When implementing one, replace this note with the real patterns and pin the emitted code in codegen tests.