en un clic
smithy-ast-model
// How to deserialize a Smithy model.json into typed C# records and navigate the shape graph
// How to deserialize a Smithy model.json into typed C# records and navigate the shape graph
Exact patterns generated code must follow to match the AWS SDK for .NET public API surface
Rules for converting Smithy shape types to .NET types, including nullability and collection defaults
Use when creating, validating, or reasoning about DevConfig files for AWS SDK for .NET changes, including deciding whether Core or Services entries are required, choosing patch vs minor bumps, and writing changelog messages.
Use when working on the AWS SDK for .NET source code itself, including Core runtime changes, service client implementations, generator or model changes, repo-specific build and validation flows, and V3/V4 branch-targeting decisions.
| name | smithy-ast-model |
| description | How to deserialize a Smithy model.json into typed C# records and navigate the shape graph |
How to read, deserialize, and navigate Smithy JSON AST (v2.0) in the SmithyDotNet generator. This is the foundation layer — all other components depend on it.
A Smithy model JSON file has three top-level keys (see Smithy JSON AST spec):
{
"smithy": "2.0",
"shapes": { "<shapeId>": { "type": "...", ... }, ... },
"metadata": { ... }
}
Every shape has an absolute ID: com.amazonaws.cloudtraildata#AuditEvent. Members append $member: com.amazonaws.cloudtraildata#AuditEvent$id.
public record ShapeId(string Namespace, string Name, string? Member = null)
{
public static ShapeId Parse(string absoluteShapeId); // splits on # and $
public string AbsoluteName => $"{Namespace}#{Name}"; // shape-only; omits $Member — for Shapes dictionary key lookups
public override string ToString() => Member is null ? AbsoluteName : $"{AbsoluteName}${Member}"; // full canonical ID
public static implicit operator string(ShapeId id) => id.ToString(); // returns full ID including member
}
Parsing rules (matching the Smithy spec):
# separates namespace from name — exactly one # required$ separates name from member (optional)Name$member (no #), ns# (empty name), ns#name$ (empty member), ns#foo#bar (multiple #)smithy.api (e.g. smithy.api#String)The JSON AST has two ways a shape ID appears as a value:
1. Plain string — inside a member object, target is a plain string:
"id": {
"target": "com.amazonaws.cloudtraildata#Uuid",
"traits": { ... }
}
The whole object is a MemberShape. The target field is just a string property on it.
2. Wrapper object — for operation input/output, service operations lists, etc., the value is a wrapper:
"input": { "target": "com.amazonaws.cloudtraildata#PutAuditEventsRequest" }
Here the entire {"target": "..."} is the value of the input property on OperationShape.
Three custom JsonConverters in SmithyDotNet.Generator.Model.Converters handle these:
ShapeIdConverter — plain string → ShapeId (for MemberShape.Target)ShapeTargetConverter — {"target": "..."} wrapper → ShapeId (for OperationShape.Input, etc.)ShapeTargetListConverter — [{"target": "..."}, ...] → List<ShapeId> (for ServiceShape.Operations, etc.)All converters are read-only (Write throws NotSupportedException). The generator never serializes models back to JSON. Use InvalidOperationException (not null-forgiving !) when a value is unexpectedly null.
All shapes derive from an abstract Shape base:
public abstract record Shape
{
public abstract string Type { get; }
[JsonPropertyName("traits")]
public Dictionary<string, JsonElement> Traits { get; init; } = [];
}
Important: Do NOT put [JsonConverter(typeof(ShapeConverter))] on Shape. This causes infinite recursion because ShapeConverter.Read calls root.Deserialize<BlobShape>(options), and BlobShape inherits Shape, which triggers the converter again. Instead, register ShapeConverter via JsonSerializerOptions.Converters.
Use [JsonPropertyName] on properties where the C# name differs in casing from the JSON key (e.g. Traits → "traits", Target → "target"). STJ is case-sensitive by default.
ShapeConverter peeks at the "type" field and dispatches:
JSON type value | C# record |
|---|---|
blob, boolean, string, byte, short, integer, long, float, double, bigInteger, bigDecimal, timestamp, document | Scalar shape records (no extra fields) |
list | ListShape — has Member (single MemberShape) |
map | MapShape — has Key and Value (both MemberShape) |
structure | StructureShape — has Members dictionary |
union | UnionShape — has Members dictionary |
enum | EnumShape — has Members (member traits carry @enumValue) |
intEnum | IntEnumShape — has Members |
service | ServiceShape — has Operations, Resources, Errors, Rename, ApiVersion |
operation | OperationShape — has Input, Output, Errors |
resource | ResourceShape — has lifecycle operations, Identifiers, Properties |
| Unknown | Returns null with a stderr warning (forward compatibility) |
MemberShape is not dispatched by ShapeConverter. It is deserialized inline by its parent shape (e.g. when STJ processes a StructureShape.Members dictionary). Its Target is a plain string in the JSON, so it uses ShapeIdConverter:
public record MemberShape : Shape
{
public override string Type => "member";
[JsonPropertyName("target")]
[JsonConverter(typeof(ShapeIdConverter))]
public required ShapeId Target { get; init; }
}
Shapes in namespace smithy.api (e.g. smithy.api#String, smithy.api#Boolean, smithy.api#Integer) are prelude shapes. They are not present in the model JSON — they are implicit. The generator skips them during shape traversal.
Traits are stored as Dictionary<string, JsonElement> on every shape. The key is the full trait ID (e.g. smithy.api#required, aws.api#service). The value is raw JSON.
Trait values are not deserialized at the model layer. They stay as JsonElement and are accessed via typed extension methods in SmithyDotNet.Generator.Model.Traits. Smithy trait accessors are organized by category: annotation traits (boolean presence checks), scalar traits (single value), and structured traits (typed records in SmithyTraitRecords.cs). AWS-specific traits (aws.* namespaces) live in AWSTraits.cs with records in AWSTraitRecords.cs. Use uppercase AWS in C# names to match .NET SDK conventions.
Structured trait records use STJ deserialization via TraitHelpers.DeserializeTrait<T>() and inherit from TraitRecord, which uses [JsonExtensionData] to capture unknown properties for forward compatibility. Use [JsonPropertyName] on record properties, matching the pattern used by shape types. ErrorTrait is the exception — it wraps a plain string value, not a JSON object.
Annotation traits have an empty object {} as their value:
"traits": { "smithy.api#required": {} }
Register ShapeConverter via options — not via [JsonConverter] attribute on Shape (see Shape Type Hierarchy above for why).
ShapeIdConverter, ShapeTargetConverter, and ShapeTargetListConverter are registered via [JsonConverter] attributes on individual properties (e.g. MemberShape.Target, OperationShape.Input) — they do NOT need to go in the options.
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = false, // Smithy JSON uses exact camelCase keys
Converters = { new ShapeConverter() }
};
var model = JsonSerializer.Deserialize<SmithyModel>(json, options);
SmithyModel.Shapes is a Dictionary<string, Shape?> keyed by the absolute shape ID string (e.g. "com.amazonaws.cloudtraildata#AuditEvent"). Unknown shape types deserialize to null values for forward compatibility.
Install the Smithy CLI (smithy) to validate models and query shapes directly. See Smithy CLI docs for installation. Use it to verify shape counts, types, and structure instead of parsing JSON manually.
Validate a model:
smithy validate --allow-unknown-traits <path-to-model.json>
Query shapes with selectors (selector spec):
smithy select --selector '<selector>' --show type --allow-unknown-traits <path-to-model.json>
--allow-unknown-traits is needed because AWS trait definitions (e.g. aws.api#service) are not bundled with the CLI.
Useful selectors:
service — all service shapesoperation — all operation shapesstructure — all structure shapes:is([id|namespace = com.amazonaws.cloudtraildata]) — shapes in a specific namespace (excludes prelude)service > operation — operations directly bound to a servicestructure > member > string — structure members targeting string shapesPowerShell caveat: selectors containing [ or $ must be single-quoted to prevent PowerShell interpretation. Use :is(...) instead of [...] attribute selectors when quoting is awkward.
ServiceShape (enforced by ModelValidator)OperationShape.Input and Output default to smithy.api#Unit when absentStructureShape.Members are the Smithy member names (camelCase), not .NET names@jsonName trait overrides the wire name; the member key is the model namesmithy.api#mixin trait) are not supported — skip them during shape traversalOperationShape.Input/Output, not solely by @input/@output traits (some models don't have these traits). Error shapes are identified by the @error trait.