Skill: Smithy to .NET Type Mapping
Type Mapping Table
| Smithy shape | .NET type | Notes |
|---|---|---|
boolean |
bool? |
Nullable |
string |
string |
Reference type, nullable by nature |
integer |
int? |
Nullable |
long |
long? |
Nullable |
float |
float? |
Nullable |
double |
double? |
Nullable |
timestamp |
DateTime? |
Nullable |
byte |
— | Not supported yet — throws. No settled .NET mapping (the current SDK never emitted byte/short) |
short |
— | Not supported yet — throws |
bigInteger |
— | Not supported yet — throws. Wider-numeric types are earmarked for a dedicated numerics extension |
bigDecimal |
— | Not supported yet — throws |
blob |
MemoryStream |
Supported as an @httpPayload body, a JSON body member, or a list element / map value (base64 string on the wire). A header or query blob still throws. Streaming blobs → Stream (@httpPayload-only; throws as a collection element) |
document |
Amazon.Runtime.Documents.Document |
SDK runtime type; (un)marshals wholesale through the runtime document transforms. Supported as a body member, list element, or map value; |
enum |
ConstantClass |
The class the ServiceEnumerationsWriter emits (see TypeMapper.EnumTypeName); marshals as a string via implicit conversion, matching C2J. Only as a member's own type — inside a collection it is plain string; see Enums in Collections |
intEnum |
int? |
No ConstantClass — C2J has no intEnum, so it maps to a plain nullable int like IntegerShape (non-nullable int as a collection element) |
list |
List<T> |
V4 default: null; see Collection Defaults. Elements: string/value-type/timestamp/enum/intEnum/structure/document or a nested list/map; value-type/timestamp elements are non-nullable (List<int>, via MapNonNullableScalar — the all-value-types-nullable rule is members-only), flipped back to nullable when the list is @sparse (List<int?>, matching C2J). An enum element collapses to string and an intEnum to plain int (int? when sparse). A non-streaming blob element maps to MemoryStream (base64 on the wire); only @streaming blob elements throw via RejectUnsupportedCollectionElement |
map |
Dictionary<string, TValue> |
V4 default: null; see Collection Defaults. Key is always string (Smithy requires it; C2J flattens enum keys too). Values follow the same rules as list elements, including @sparse nullability |
structure |
Generated class | See structure rules below |
union |
Generated class | Generated as regular structure (matches current SDK) |
Nullability Rules
The AWS SDK for .NET V4 convention:
- All value types are nullable —
bool?,int?,DateTime?, etc. regardless of@requiredor@defaulttraits - Reference types (
string,MemoryStream) — nullable by nature, no?suffix needed in the type declaration - Collections — V4 default:
null; see Collection Defaults section - The
@defaulttrait does NOT change nullability — this matches current SDK behavior - The
@requiredtrait does NOT change nullability — it only affects the[AWSProperty(Required=true)]attribute
Opt-in Strict Nullability (Non-AWS Use)
The generator will support an opt-in mode that respects Smithy's nullability traits (@required, @default, @clientOptional) for non-AWS Smithy models. When disabled (the default for AWS), all value types are nullable regardless of traits.
Collection Defaults
Collections use AWSConfigs.InitializeCollections for SDK V4 backwards compatibility. The
generator emits an auto-property initialized to AWSConfigs.InitializeCollections ? new List<T>() : null,
plus an internal IsSet{Property}() method that the AWS SDK runtime (and marshallers) call — exact
emitted shape in sdk-conventions "Collection Properties".
When AWSConfigs.InitializeCollections is false (V4 default), collections start as null,
and an empty list still counts as "set" (the caller cleared the value). When true (V3 compat),
collections start empty and an empty list counts as "not set". The IsSet method encodes that
rule so callers — including the public reflection API AWSSDKUtils.IsPropertySet — see the
correct answer in both modes.
Enums in Collections
An enum surfaces as its ConstantClass only as a structure member's own type. As a list element, a
map key, or a map value it is plain string:
| Smithy | .NET |
|---|---|
member targeting Status |
Status (the ConstantClass) |
list<Status> |
List<string> |
map<Status, Status> |
Dictionary<string, string> |
list<list<Status>> |
List<List<string>> |
This matches C2J: Member.DetermineType passes treatEnumsAsString: true when it recurses into a list
member or a map key/value, and only the member's own call passes false. Typing an element as its
ConstantClass would be a public-API divergence (Amazon.Lambda.Model.CreateFunctionRequest.Architectures
and friends are List<string> in the shipped SDK).
TypeMapper.CollectionElementTarget is the single place the collapse happens. It feeds the .NET type name
(MapType via ElementTarget), the element TypeDescriptor (ResolveType with isCollectionValue: true),
and PaginationResolver's items element type — so the descriptor's IsEnum is never set on an element, the
writers see a plain string leaf, and a paginator's flattened enumerable agrees with the List<string>
property it reads from. It takes an already-resolved Shape rather than a ShapeId precisely so
PaginationResolver can call it: that runs off a ServiceIndex and has no GenerationContext.
Resolves_EnumItemsElement_AsString pins the paginator's side.
A paginator's items element type is a collection element, so it follows the same nullability rule —
PaginationResolver derives it via TypeMapper.MapScalarElement (non-nullable, nullable when the list
is @sparse), the same call the property type goes through.
An intEnum element maps to a plain non-nullable int (like IntegerShape), so list<intEnum> is
List<int> — it does not fail loud. A non-streaming blob element maps to MemoryStream and
base64-encodes exactly like a blob body member (list<blob> → List<MemoryStream>). A document element
is supported and passes the check. The only leaf RejectUnsupportedCollectionElement still rejects is a
@streaming blob: it maps to Stream, is @httpPayload-only, and StringUtils.WriteBase64StringValue
takes a MemoryStream, so it can't ride the element path.
C2J's Customizations.OverrideTreatEnumsAsString can flip this per shape. That hook is not implemented
(the loader rejects it), so the default (true, i.e. string) is the only behavior. The one enum
customization that is implemented is emitPropertyName (keyed by wire value): TypeMapper.EnumMemberName
returns it over the derived constant name.
Constrained Shapes
Smithy allows constrained shapes (e.g. a string shape named Uuid with @length or @pattern). These do NOT get wrapper classes — they map to their underlying .NET type:
| Smithy | .NET |
|---|---|
Uuid (string shape with constraints) |
string |
AuditEvents (list shape) |
List<AuditEvent> inline |
ChannelArn (string shape) |
string |
The constraints flow through to [AWSProperty] attributes on the member that references the shape. When resolving [AWSProperty], check traits on both the member and its target shape:
@requiredis on the member@length/@rangeare typically on the target shape (e.g.AuditEventslist has@length(min:1, max:100), soPutAuditEventsRequest.AuditEventsgets[AWSProperty(Required=true, Min=1, Max=100)])
Error Shape Naming
Smithy error shapes often omit the Exception suffix. The naming rules (matching the existing generator's ExceptionShape.Name logic):
- Ends with
Exception→ keep as-is (e.g.UnsupportedOperationException) - Ends with
Fault→ replaceFaultwithException - Otherwise → append
Exception(e.g.ChannelNotFound→ChannelNotFoundException)
Error Shape Members
Error shapes have a message member in the Smithy model, but the generated exception class does not expose it as a property. The message is passed to System.Exception via the constructor and inherited as Exception.Message. The generator filters message out everywhere — the property set, the serialization block, and the unmarshaller alike — since Exception.Message already covers it.
Two base-class adjustments follow (matching ExceptionShape.Members, Member.cs, and StructureGenerator.tt):
- A member named
errorTypeis renamed to the propertyRequestErrorType— property name only; the wire name stayserrorType— so it doesn't hideAmazonServiceException.ErrorType, whose type is theErrorTypeenum rather than the member's own type. - A member named
Retryable(on an exception) is emitted with thenewmodifier, hidingAmazonServiceException.Retryable— CloudHSM's exception models it with a different return type.
Independently of the exception-only rules, a member named Equals on any structure is emitted with new to hide object.Equals(object) (matches StructureGenerator.tt's unconditional Equals check). This is set in TypeMapper.ResolveMembers and flows through every writer.
A response member named ContentLength is not shadowed but omitted from the response class entirely — AmazonWebServiceResponse already declares it and the unmarshaller assigns the inherited property. That rule is writer-level (OperationWriter.WriteResponse, not TypeMapper); see sdk-conventions.
RequestId and ErrorCode get a narrower treatment than message. AmazonServiceException already declares them, so the generator emits no property (one would shadow the base; C2J's StructureGenerator.tt skips them in its property loop). But unlike message they are not filtered from serialization or unmarshalling — C2J's ExceptionSerialization.t4 and JsonRPCExceptionUnmarshaller.tt loop ExceptionShape.Members, which drops only message — so the inherited property is still serialized and read from the error body. Hence ExceptionWriter.ResolveSerializedMembers (serialization block + unmarshaller) keeps them, while the property set is that same set with RequestId/ErrorCode filtered out inline in WriteException. Every other member — including one whose name collides with a non-omitted inherited property (e.g. StatusCode, InnerException) — is emitted as-is as a plain shadowing property and also read from the error body, exactly as C2J does.
Resolving Member Types
To get the .NET type for a structure member:
- Get the member's
Targetshape ID GenerationContext.Resolve(target)returns the shape. Prelude shapes (smithy.api#String, etc.) are not in the model's shape map, butResolvefalls back toPreludeShapes, so callers never special-case them — a preludeStringcomes back as aStringShapelike any other. Map the resolved shape'stypefrom the table:- Simple/scalar shape → map its
typefrom the table - Structure/union → use the generated class name
- List →
List<{resolve member.Target}> - Map →
Dictionary<string, {resolve value.Target}>(an enum key isstring, never its ConstantClass) - Enum → its
ConstantClasssubclass as a member's own type,stringinside a collection (see Enums in Collections) - Constrained string shapes (e.g.
Uuid) resolve to aStringShape→string(no wrapper)
- Simple/scalar shape → map its
Prelude Shape Mapping
These shapes are implicit (not in the model JSON) and map directly. See the Type Mapping Table above
for the positions each one is supported in — Blob (non-streaming) and Document map here and are
accepted as body members and collection elements, but not everywhere (e.g. a header/query blob throws):
| Prelude shape ID | .NET type |
|---|---|
smithy.api#String |
string |
smithy.api#Boolean |
bool? |
smithy.api#Integer |
int? |
smithy.api#Long |
long? |
smithy.api#Float |
float? |
smithy.api#Double |
double? |
smithy.api#Blob |
MemoryStream |
smithy.api#Timestamp |
DateTime? |
smithy.api#Document |
Amazon.Runtime.Documents.Document |
smithy.api#Unit |
(no type — used for operations with no input/output) |