Skip to content

Emitter Development Guide

The emitter transforms TypeSpec definitions into runtime code for C#, Python, TypeScript, and Go. This guide covers how to modify code generation.

  • Directoryagentschema-emitter/
    • Directorysrc/
      • emitter.ts entry point - dispatches to generators
      • ast.ts TypeSpec AST parsing (TypeNode, PropertyNode, BaseTestContext)
      • test-context.ts shared test context builder for all languages
      • decorators.ts custom decorator handling
      • utilities.ts helper functions
      • csharp.ts C# code generator
      • python.ts Python code generator
      • typescript.ts TypeScript code generator
      • go.ts Go code generator
      • markdown.ts documentation generator
      • Directorytemplates/ Nunjucks templates
        • Directorycsharp/
        • Directorypython/
        • Directorytypescript/
        • Directorygo/
    • Directorydist/ compiled output (git-ignored)
flowchart LR
    A[TypeSpec AST] --> B[emitter.ts]
    B --> C[ast.ts<br/>resolveModel]
    C --> D[TypeNode Tree]
    D --> E[csharp.ts]
    D --> F[python.ts]
    D --> G[typescript.ts]
    D --> N[go.ts]
    E --> H[templates/csharp/*.njk]
    F --> I[templates/python/*.njk]
    G --> J[templates/typescript/*.njk]
    N --> O[templates/go/*.njk]
    H --> K[runtime/csharp/]
    I --> L[runtime/python/]
    J --> M[runtime/typescript/]
    O --> P[runtime/go/]
    
    subgraph "Test Generation"
        TC[test-context.ts<br/>buildBaseTestContext] --> E
        TC --> F
        TC --> G
        TC --> N
    end

The ast.ts file defines the core data structures passed to templates:

class TypeNode {
typeName: { namespace: string; name: string };
description: string;
base: TypeName | null; // Parent type
childTypes: TypeNode[]; // Derived types
properties: PropertyNode[];
isAbstract: boolean;
discriminator?: string;
alternates: Alternative[]; // Shorthand representations
}
class PropertyNode {
name: string;
typeName: TypeName;
description: string;
isScalar: boolean;
isOptional: boolean;
isCollection: boolean;
samples: SampleEntry[];
defaultValue: any;
}
  1. emitter.ts receives the TypeSpec program
  2. resolveModel() builds the TypeNode tree from the root object
  3. enumerateTypes() yields all types for code generation
  4. Language-specific generators render templates for each type

Templates use Nunjucks syntax. Each language has templates for:

FilePurpose
file.{ext}.njkMain class definitions
test.{ext}.njkTest file generation (uses standardized context)
_macros.njkShared template macros
context.*.njkLoadContext/SaveContext classes

All test templates use the shared buildBaseTestContext() function from test-context.ts. This ensures consistent field names across all languages:

// In your language generator
import { buildBaseTestContext, yourLanguageTestOptions } from "./test-context.js";
function buildTestContext(node: TypeNode, packageName: string): BaseTestContext {
return buildBaseTestContext(node, packageName, yourLanguageTestOptions);
}

Standardized field names in test templates:

FieldDescription
validationsArray of property assertions (not validation)
delimiterQuote character for strings (not delimeter)
scalarTypeThe scalar type name (not scalar)
isOptionalWhether property is optional (not isPointer)
packagePackage/namespace (not packageName)
{# Comment #}
{{ variable }} {# Output variable #}
{{ value | lower }} {# Apply filter #}
{% if condition %}...{% endif %} {# Conditional #}
{% for item in items %}...{% endfor %} {# Loop #}
{%- ... -%} {# Trim whitespace #}

Rendering property names:

{# C# - PascalCase #}
{{ renderName(prop.name) }}
{# Python - snake_case (handled in generator) #}
{{ prop.name }}
{# TypeScript - camelCase #}
{{ prop.name }}
{# Go - PascalCase (exported) #}
{{ renderName(prop.name) }}

Type mapping:

{% if prop.isScalar %}
{{ typeMapper[prop.typeName.name] }}
{% else %}
{{ prop.typeName.name }}
{% endif %}

Handling optionals:

{# C# #}
{{ type }}{% if prop.isOptional %}?{% endif %}
{# Python #}
Optional[{{ type }}]
{# TypeScript #}
{{ prop.name }}{% if prop.isOptional %}?{% endif %}: {{ type }}
{# Go - uses pointer for optional #}
{% if prop.isOptional %}*{% endif %}{{ type }}

Example: Add a new property to generated classes

Section titled “Example: Add a new property to generated classes”
  1. Edit the TypeScript generator (e.g., csharp.ts):
// In buildClassContext or similar
const context = {
node,
newFeature: computeNewFeature(node),
// ...
};
  1. Update the template (e.g., templates/csharp/file.cs.njk):
{% if newFeature %}
/// <summary>New feature documentation</summary>
public string NewFeature => "{{ newFeature }}";
{% endif %}
  1. Rebuild and test:
Terminal window
cd agentschema-emitter
npx tsc
cp -r src/templates dist/src/
cd ../agentschema
npm run generate
cd ../runtime/csharp && dotnet test

For test data, use the YAML library with proper escaping:

import * as YAML from "yaml";
const doc = new YAML.Document(sample);
YAML.visit(doc, {
Scalar(key, node) {
if (typeof node.value === 'string') {
const str = node.value as string;
// Quote strings with special characters
if (str.includes('\n') || str.includes('#')) {
node.type = 'QUOTE_DOUBLE';
}
}
}
});
const yaml = doc.toString({ indent: 2, lineWidth: 0 });
Terminal window
cd agentschema-emitter
# Compile TypeScript
npx tsc
# Copy templates (REQUIRED - templates aren't compiled)
cp -r src/templates dist/src/
# On Windows PowerShell:
Copy-Item -Recurse -Force src/templates dist/src/

After any emitter change, test all four runtimes:

Terminal window
# C#
cd runtime/csharp && dotnet test
# Python
cd runtime/python/agentschema && uv run pytest tests/
# TypeScript
cd runtime/typescript/agentschema && npm test
# Go
cd runtime/go/agentschema && go test ./...
  1. Print AST data: Add console.log(JSON.stringify(node, null, 2)) in generators
  2. Check template context: Log the context object before rendering
  3. Inspect generated files: Look at output in runtime/ to verify changes
  4. Run single runtime: Focus on one language while iterating

Forgot to copy templates to dist/src/. Run:

Terminal window
cp -r src/templates dist/src/

Check type mappers in the language generator (e.g., csharpTypeMapper).

Update the imports array in the template context or _macros.njk.