Free-form markdown from an LLM is a demo. Production codegen needs a contract: JSON Schema → validated object → deterministic template/AST transform → files on disk. If the shape is wrong, you never touch the filesystem.
⚡ TL;DR: Define a strict JSON Schema for every codegen artifact (route, migration, React component props). Use Bedrock/
response_format/tool-args validation, then Ajv (or zod) before write. Map JSON → ts-morph/AST or Handlebars — not “paste this into the repo.” Fail closed on additionalProperties. See Cursor Rules for TypeScript Monorepos and LLM Coding Agents on AWS.
Schema first, prose never on the write path
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://acme.dev/schemas/http-route.json",
"type": "object",
"additionalProperties": false,
"required": ["method", "path", "handlerName", "auth", "responseType"],
"properties": {
"method": { "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"] },
"path": { "type": "string", "pattern": "^/v[0-9]+/[a-z0-9-{}/]+$" },
"handlerName": { "type": "string", "pattern": "^[A-Z][A-Za-z0-9]+Handler$" },
"auth": { "enum": ["none", "session", "m2m"] },
"responseType": { "type": "string", "pattern": "^[A-Z][A-Za-z0-9]+$" },
"queryParams": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name", "type"],
"properties": {
"name": { "type": "string", "pattern": "^[a-z][a-zA-Z0-9]*$" },
"type": { "enum": ["string", "int", "bool"] }
}
}
}
}
}
import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true, strict: true });
const validate = ajv.compile(routeSchema);
export function parseRoute(raw: unknown) {
if (!validate(raw)) {
// ❌ Never write files when invalid
throw new Error("schema_violation:" + ajv.errorsText(validate.errors));
}
return raw as RouteSpec;
}
Force the model into the schema
# Bedrock Converse with tool schema as the only output channel
TOOL = {
"toolSpec": {
"name": "emit_route_spec",
"description": "Emit a single HTTP route spec",
"inputSchema": {"json": route_schema_dict},
}
}
resp = bedrock.converse(
modelId=MODEL,
toolConfig={"tools": [TOOL], "toolChoice": {"tool": {"name": "emit_route_spec"}}},
messages=[{"role": "user", "content": [{"text": prompt}]}],
)
# Extract toolUse input → validate again client-side
✅ toolChoice specific forces structured args.
❌ “Return JSON in a markdown fence” — parsers will eventually fail on a witty preface.
Deterministic compile step
// codegen/compileRoute.ts — JSON → source via ts-morph (illustrative)
import { Project } from "ts-morph";
export function compileRoute(spec: RouteSpec, outFile: string) {
const project = new Project();
const sf = project.createSourceFile(outFile, "", { overwrite: true });
sf.addImportDeclaration({
moduleSpecifier: "@/http",
namedImports: ["Router", spec.responseType],
});
sf.addFunction({
name: spec.handlerName,
isExported: true,
parameters: [{ name: "req", type: "Request" }, { name: "res", type: `Response<${spec.responseType}>` }],
statements: writer => {
writer.writeLine(`// auth=${spec.auth}`);
writer.writeLine(`throw new Error("TODO implement ${spec.method} ${spec.path}");`);
},
});
sf.saveSync();
}
Keep transforms pure: same JSON → same bytes (stable formatting with prettier in CI).
Reject before git add
# scripts/codegen-guard.sh
node dist/codegen/run.js --spec "$1" --dry-validate
node dist/codegen/run.js --spec "$1" --write
pnpm exec tsc -p packages/api --noEmit
pnpm exec eslint "$(jq -r .outFile "$1")"
# ❌ Don't: agent writes .ts directly from prose
Wire review bots to reject PRs where new handlers lack matching schema artifacts — AI Code Review Bots.
Version the schema like an API
Bump $id / version when fields change. Keep a fixtures corpus: golden JSON → golden .ts snapshots in CI so prompt drift cannot silently change output shape.
Closing checklist
✅ Dos
– ✅ JSON Schema with additionalProperties: false
– ✅ Force tool/structured output; re-validate client-side
– ✅ Compile via AST/templates, not string paste
– ✅ Typecheck + lint generated files in the same job
– ✅ Snapshot golden outputs for schema versions
❌ Don’ts
– ❌ Don’t write to disk on schema failure
– ❌ Don’t accept markdown-fenced “JSON”
– ❌ Don’t let the model invent field names outside the schema
– ❌ Don’t skip prettier/tsc on generated paths
– ❌ Don’t hand-edit generated files without updating the spec
Related reading
- Cursor Rules for TypeScript Monorepos: Make AI Edits Stick
- LLM Coding Agents on AWS: Safe Tool Sandboxes with Lambda
- AI Code Review Bots: IAM, Secrets, and Least-Privilege Pipelines
- Amazon Bedrock Agents: Tool Use, Memory, and Production Guardrails
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
