This document describes the architecture of the Smithy Unison code generator, which follows Smithy's recommended DirectedCodegen pattern for extensibility and maintainability.
Reference: Creating a Code Generator
The code generator transforms Smithy service models into Unison client modules (.u files). It uses a plugin-based architecture that allows customization at multiple points in the generation process.
┌────────────────────────────────────────────────────────────────────┐
│ Smithy Build │
│ ┌───────────────┐ ┌──────────────────┐ ┌─────────────────┐ │
│ │ smithy-build │───▶│ UnisonCodegen │───▶│ Generated │ │
│ │ .json │ │ Plugin │ │ .u files │ │
│ └───────────────┘ └──────────────────┘ └─────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
The main entry point that implements SmithyBuildPlugin. It orchestrates code generation using CodegenDirector.
public final class UnisonCodegenPlugin implements SmithyBuildPlugin {
@Override
public void execute(PluginContext context) {
CodegenDirector<UnisonWriter, UnisonIntegration, UnisonContext, UnisonSettings> director =
new CodegenDirector<>();
director.directedCodegen(new UnisonGenerator());
director.integrationClass(UnisonIntegration.class);
// ... configuration ...
director.run();
}
}Immutable configuration object parsed from smithy-build.json:
public final class UnisonSettings {
private final ShapeId service; // Target service shape
private final String namespace; // Output namespace (e.g., "aws.s3")
private final String outputDir; // Output directory
private final String protocol; // Optional protocol override
}Implements CodegenContext to provide access to all generation dependencies:
public final class UnisonContext implements CodegenContext<UnisonSettings, UnisonWriter, UnisonIntegration> {
private final Model model;
private final UnisonSettings settings;
private final SymbolProvider symbolProvider;
private final FileManifest fileManifest;
private final List<UnisonIntegration> integrations;
private final WriterDelegator<UnisonWriter> writerDelegator;
}Implements DirectedCodegen to handle shape-by-shape code generation:
public class UnisonGenerator implements DirectedCodegen<UnisonContext, UnisonSettings, UnisonIntegration> {
@Override
public SymbolProvider createSymbolProvider(CreateSymbolProviderDirective<UnisonSettings> directive) {
return new UnisonSymbolProvider(directive.model(), directive.settings());
}
@Override
public void generateService(GenerateServiceDirective<UnisonContext, UnisonSettings> directive) {
ClientModuleWriter writer = ClientModuleWriter.fromContext(directive.context());
writer.generate(); // Handles type generation and runtime module copying
}
}Extends SymbolWriter for Unison-specific code generation:
public class UnisonWriter extends SymbolWriter<UnisonWriter, UnisonImportContainer> {
public UnisonWriter(String namespace) {
super(new UnisonImportContainer());
putFormatter('T', this::formatUnisonType); // $T for types
putFormatter('N', this::formatUnisonName); // $N for names
}
public UnisonWriter writeRecordType(String typeName, List<TypeField> fields) { ... }
public UnisonWriter writeUnionType(String typeName, String typeParams, List<Variant> variants) { ... }
public UnisonWriter writeSignature(String name, String signature) { ... }
public UnisonWriter writeFunction(String name, String params, Runnable body) { ... }
}Orchestrates client module generation with conditional logic based on service type:
public final class ClientModuleWriter {
public void generate() throws IOException {
// Detect protocol and AWS service
AwsProtocol protocol = AwsProtocolDetector.detectProtocol(service);
boolean isAws = copier.isAwsService(service, protocol);
// Generate Config type (AWS-style or generic)
if (isAws) {
generateAwsConfigTypes(writer);
} else {
generateGenericConfigType(writer);
}
// Generate model types for non-AWS services
if (!useProtocolGenerator) {
generateModelTypes(writer);
}
// Generate operations (including resource operations)
// For REST-JSON, generate nested structure serializers upfront
// Copy runtime modules (only for AWS services)
copyRuntimeModules(protocol);
}
}Handles conditional copying of runtime modules based on service and protocol:
public final class RuntimeModuleCopier {
// AWS service detection using traits
public boolean isAwsService(ServiceShape service, AwsProtocol protocol) {
return service.findTrait(AWS_SERVICE_TRAIT).isPresent()
|| service.findTrait(AWS_SIGV4_TRAIT).isPresent()
|| protocol != AwsProtocol.UNKNOWN;
}
// Protocol-aware module copying
public List<String> copyAwsModulesForProtocol(AwsProtocol protocol, ServiceShape service) {
// Core modules: aws_sigv4.u, aws_config.u, aws_credentials.u, aws_http.u, aws_http_bridge.u
// XML protocol: aws_xml.u
// S3 service: aws_s3.u
}
}Implements SymbolProvider for Smithy-to-Unison type mapping:
public final class UnisonSymbolProvider implements SymbolProvider {
@Override
public Symbol toSymbol(Shape shape) {
return shape.accept(new UnisonTypeVisitor());
}
public static String toUnisonTypeName(String name) {
// Keep PascalCase for types
return name;
}
public static String toUnisonFunctionName(String name) {
// Convert PascalCase to camelCase for functions
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
}
}Extends SmithyIntegration for pluggable extensions:
public interface UnisonIntegration
extends SmithyIntegration<UnisonSettings, UnisonWriter, UnisonContext> {
default String name() { return getClass().getCanonicalName(); }
default byte priority() { return 0; }
default void preprocessModel(UnisonContext context) {
// Default: no-op
}
default void postprocessGeneration(UnisonContext context) {
// Default: no-op
}
}| Integration | Status | Purpose |
|---|---|---|
SigV4Generator |
✅ | Generates AWS SigV4 request signing code |
RuntimeModuleCopier |
✅ | Copies protocol-specific runtime modules |
AwsRetryIntegration |
Planned | Copies retry logic module |
Integrations are discovered via Java's ServiceLoader mechanism:
src/main/resources/META-INF/services/io.smithy.unison.codegen.UnisonIntegration
public interface ProtocolGenerator {
ShapeId getProtocol();
void generateOperation(OperationShape operation, UnisonWriter writer, UnisonContext context);
void generateRequestSerializer(OperationShape operation, UnisonWriter writer, UnisonContext context);
void generateResponseDeserializer(OperationShape operation, UnisonWriter writer, UnisonContext context);
}| Protocol | Generator Class | Status | Services |
|---|---|---|---|
| REST-XML | RestXmlProtocolGenerator |
✅ | S3, CloudFront, Route 53 |
| AWS JSON 1.0/1.1 | AwsJsonProtocolGenerator |
✅ | DynamoDB, Lambda, Kinesis |
| REST-JSON | RestJsonProtocolGenerator |
✅ | EventBridge, Step Functions, API Gateway, Lambda |
| AWS Query | AwsQueryProtocolGenerator |
✅ | SQS, SNS, RDS, CloudWatch |
| EC2 Query | Ec2QueryProtocolGenerator |
✅ | EC2, Auto Scaling, ELB Classic |
Request Format: Form-encoded (URL-encoded) parameters sent via POST
Action=CreateTopic&Name=MyTopic&Version=2010-03-31
Response Format: XML with nested result wrapper
<CreateTopicResponse>
<CreateTopicResult>
<TopicArn>arn:aws:sns:us-east-1:123456789012:MyTopic</TopicArn>
</CreateTopicResult>
</CreateTopicResponse>Key Features:
- Parameters serialized using dot notation:
Tags.member.1.Key=env - Lists numbered starting from 1:
.member.1,.member.2 - Maps serialized as entries:
.entry.1.key,.entry.1.value - Response wrapper:
<OperationNameResponse><OperationNameResult> - Error format:
<ErrorResponse><Error><Code>...</Code></Error></ErrorResponse>
Implementation: AwsQueryProtocolGenerator.java
EC2 Query extends AWS Query with these differences:
Response Format: Simpler XML structure (no nested Result element)
<RunInstancesResponse>
<requestId>...</requestId>
<reservationId>...</reservationId>
...
</RunInstancesResponse>Error Format: Different wrapper structure
<Response>
<Errors>
<Error>
<Code>InvalidParameterValue</Code>
<Message>...</Message>
</Error>
</Errors>
</Response>Implementation: Ec2QueryProtocolGenerator.java (extends AwsQueryProtocolGenerator)
┌───────────────────────────────────────────────────────────────────┐
│ Code Generation Flow │
├───────────────────────────────────────────────────────────────────┤
│ │
│ 1. Plugin Initialization │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Load Model │───▶│ Parse │───▶│ Create │ │
│ │ │ │ Settings │ │ Context │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ 2. Integration Discovery (SPI) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ServiceLoader│───▶│ Sort by │───▶│ Apply │ │
│ │ .load() │ │ Priority │ │ preprocessors│ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ 3. Shape Generation (DirectedCodegen) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Service │───▶│ Structures │───▶│ Unions/Enums │ │
│ │ generateSvc │ │ generateStruct│ │ generateEnum │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ 4. Protocol-Specific Generation │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Detect │───▶│ Select │───▶│ Generate │ │
│ │ Protocol │ │ Generator │ │ Operations │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ 5. File Output │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Flush │───▶│ Copy Runtime │───▶│ Write .u │ │
│ │ Writers │ │ Modules │ │ Files │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────┘
The generator conditionally generates AWS-specific code based on service traits:
aws.api#servicetrait - Definitive AWS service markeraws.auth#sigv4trait - AWS authentication requirement- Protocol detection - AWS protocol traits (
aws.protocols#*)
| Feature | AWS Service | Non-AWS Service |
|---|---|---|
| Config type | endpoint, region, credentials, usePathStyle |
endpoint, headers |
| Credentials type | Generated | Not generated |
| Runtime modules | Copied (protocol-specific) | Not copied |
| Model types | Via protocol generator | Via generateModelTypes() |
| Operations | Full implementation (if protocol supported) | Stub implementation |
Status: ✅ Fully Implemented
Generator: RestJsonProtocolGenerator.java
Trait: aws.protocols#restJson1
Services: EventBridge, Step Functions, API Gateway, Lambda, AppSync, IoT, Cognito, WAF
The REST-JSON protocol combines RESTful HTTP bindings with JSON request/response serialization. It uses HTTP verbs, URI paths, query strings, and headers for operation routing while serializing structured data as JSON.
The generator extracts HTTP bindings from Smithy traits to determine where each input member should be placed:
| Trait | Location | Example |
|---|---|---|
@httpLabel |
URI path | /functions/{FunctionName}/invocations |
@httpQuery |
Query string | ?maxResults=100&nextToken=abc |
@httpHeader |
HTTP header | X-Amz-Target: EventBridge.PutEvents |
@httpPayload |
HTTP body (raw) | Binary or text payload |
| (none) | HTTP body (JSON) | {"Detail": "...", "DetailType": "..."} |
Binding Priority:
- Members with explicit HTTP traits (
@httpLabel,@httpQuery,@httpHeader,@httpPayload) - Remaining members serialize to JSON body
┌─────────────────────────────────────────────────────────────┐
│ REST-JSON Request Generation │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. HTTP Binding Detection │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Scan Input │───▶│ Group by │───▶│ Validate │ │
│ │ Members │ │ Trait Type │ │ Placement │ │
│ └──────────────┘ └──────────────┘ └────────────┘ │
│ │
│ 2. URL Building │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Substitute │───▶│ Append Query │ │
│ │ Path Params │ │ Parameters │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 3. Header Construction │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Extract │───▶│ Add Content- │ │
│ │ @httpHeader │ │ Type │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 4. Body Serialization │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Collect Body │───▶│ Serialize to │───▶│ UTF-8 │ │
│ │ Members │ │ JSON │ │ Encode │ │
│ └──────────────┘ └──────────────┘ └────────────┘ │
│ │
│ 5. SigV4 Signing │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Sign Request │───▶│ Add Auth │ │
│ │ (aws.sigv4) │ │ Headers │ │
│ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ REST-JSON Response Deserialization │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Status Check │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Check Status │───▶│ < 300: │───▶│ Parse │ │
│ │ Code │ │ Success │ │ Response │ │
│ └──────────────┘ │ >= 300: │ └────────────┘ │
│ │ Error │ │
│ └──────────────┘ │
│ ↓ │
│ 2. Error Parsing (if >= 300) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Parse JSON │───▶│ Extract │ │
│ │ Body │ │ __type/code │ │
│ └──────────────┘ └──────────────┘ │
│ ↓ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Extract │───▶│ Map to │ │
│ │ message │ │ ServiceError │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 3. Success Parsing │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Parse JSON │───▶│ Extract HTTP │───▶│ Construct │ │
│ │ Body │ │ Headers │ │ Output │ │
│ └──────────────┘ └──────────────┘ └────────────┘ │
│ │
│ 4. Deserializer Generation │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Generate │───▶│ Generate │───▶│ Generate │ │
│ │ List/Map │ │ Enum/Union │ │ Structure │ │
│ │ Deserializers│ │ Deserializers│ │ Deserial. │ │
│ └──────────────┘ └──────────────┘ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
The generator produces complete JSON deserializer functions for:
- Structures: Field-by-field parsing with exception handling
- Enums: String-to-enum conversion with validation
- Unions: Tagged union deserialization
- Lists: Element-wise deserialization with
mapWithException - Maps: Key-value pair deserialization with
mapPairsWithException
REST-JSON errors are returned as JSON with HTTP status >= 300. The error code can appear in multiple locations:
{
"__type": "ResourceNotFoundException",
"message": "The resource was not found"
}Or:
{
"code": "ResourceNotFoundException",
"message": "The resource was not found"
}The generator handles all variations:
__typefield (primary)codefield (secondary)Codefield (alternate casing)Typefield (alternate location)
Unison has reserved keywords like type that may appear as field names in AWS models. The generator escapes these using backticks:
-- Field accessor with escaped keyword
Condition.`type` condition
-- Lambda parameter with escaped keyword
Optional.flatMap (`type` -> ...) typeOpt
The REST-JSON protocol requires these runtime modules:
aws_restjson.u- URL building, path/query parameter handlingaws_json.u- JSON serialization/deserializationaws_json_bridge.u- JSON parsing utilities (parseFloat,parseBlob,mapWithException,mapPairsWithException)aws_http.u- HTTP client with retryaws_sigv4.u- SigV4 request signing
Unison generates .u scratch files that users will add to their codebase via UCM (Unison Codebase Manager).
Reference: https://www.unison-lang.org/docs/tooling/project-workflows/
| Smithy Type | Unison Type |
|---|---|
string |
Text |
integer, long, short, byte |
Int |
float, double |
Float |
boolean |
Boolean |
blob |
Bytes |
timestamp |
Text |
list<T> |
[T] |
map<K, V> |
Map K V |
structure |
Record type |
union |
Sum type |
- Types: PascalCase (e.g.,
GetObjectInput) - Functions: camelCase (e.g.,
getObject) - Fields: camelCase (e.g.,
bucketName)