Skip to content

Add $ref support to all four language code generators#1062

Open
stephentoub wants to merge 1 commit intomainfrom
stephentoub/add-ref-support-codegen
Open

Add $ref support to all four language code generators#1062
stephentoub wants to merge 1 commit intomainfrom
stephentoub/add-ref-support-codegen

Conversation

@stephentoub
Copy link
Copy Markdown
Collaborator

@stephentoub stephentoub commented Apr 10, 2026

Summary

Enable JSON Schema $ref for type deduplication across all SDK code generators (TypeScript, Python, Go, C#). This allows the runtime to declare a shared type once using $ref and have each generator produce a single deduplicated type per language.

Changes

scripts/codegen/utils.ts

  • Add resolveRef(), refTypeName(), collectDefinitions() helpers
  • Normalize $defs to definitions in postProcessSchema for cross-draft compatibility
  • Add definitions/$defs fields to ApiSchema interface

scripts/codegen/typescript.ts

  • Build a single combined schema with shared definitions and compile once via unreachableDefinitions, instead of per-method compilation
  • Preserves @experimental JSDoc annotations via post-processing

scripts/codegen/python.ts

  • Include all definitions alongside SessionEvent for quicktype $ref resolution
  • Include shared API definitions in each RPC addSource call

scripts/codegen/go.ts

  • Add $ref handling to the custom Go session events generator (resolveGoPropertyType) with definitions on GoCodegenCtx
  • Include shared API definitions in RPC combined schema for quicktype $ref resolution

scripts/codegen/csharp.ts

  • Add $ref handling to resolveSessionPropertyType and resolveRpcType
  • Generate classes for referenced types on demand using module-level sessionDefinitions/rpcDefinitions state
  • Handle $ref in array items

Motivation

The copilot-agent-runtime is moving to use $ref for type deduplication in tool schemas. Without this change, generators would either fail to resolve $ref pointers or inline duplicate type definitions.

Enable JSON Schema $ref for type deduplication across all SDK code
generators (TypeScript, Python, Go, C#).

Changes:
- utils.ts: Add resolveRef(), refTypeName(), collectDefinitions() helpers;
  normalize $defs to definitions in postProcessSchema
- typescript.ts: Build combined schema with shared definitions and compile
  once via unreachableDefinitions, instead of per-method compilation
- python.ts/go.ts: Include all definitions alongside SessionEvent for
  quicktype  resolution; include shared API defs in RPC combined schema
- csharp.ts: Add  handling to resolveSessionPropertyType and
  resolveRpcType; generate classes for referenced types on demand

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@stephentoub stephentoub requested a review from a team as a code owner April 10, 2026 22:55
Copilot AI review requested due to automatic review settings April 10, 2026 22:55
@stephentoub stephentoub changed the title Add $ ef support to all four language code generators Add $ref support to all four language code generators Apr 10, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the multi-language schema-to-SDK generators (TypeScript, Python, Go, C#) to support JSON Schema $ref so shared definitions can be deduplicated and emitted once per language.

Changes:

  • Adds shared $ref utilities (resolveRef, refTypeName, collectDefinitions) and expands ApiSchema to surface schema definitions.
  • Refactors TypeScript RPC generation to compile a single combined schema (shared definitions + all RPC param/result types) in one pass.
  • Updates Python/Go quicktype inputs and C# custom emitters to include/resolve referenced definitions.
Show a summary per file
File Description
scripts/codegen/utils.ts Adds $ref helpers and attempts $defsdefinitions normalization in schema post-processing.
scripts/codegen/typescript.ts Generates RPC types from one combined schema to dedupe $ref-shared types; re-adds experimental annotations post-compile.
scripts/codegen/python.ts Includes shared definitions for quicktype to resolve $ref during session-event and RPC generation.
scripts/codegen/go.ts Adds $ref handling to Go session-events custom generator; includes shared definitions for RPC quicktype generation.
scripts/codegen/csharp.ts Adds $ref handling for session events + RPC generation, with on-demand referenced type emission.

Copilot's findings

Comments suppressed due to low confidence (1)

scripts/codegen/python.ts:23

  • The import list includes isRpcMethod twice, which will cause a TypeScript compile error (“Duplicate identifier 'isRpcMethod'”). Remove the duplicate specifier.
import {
    getApiSchemaPath,
    getSessionEventsSchemaPath,
    isRpcMethod,
    postProcessSchema,
    writeGeneratedFile,
    collectDefinitions,
    isRpcMethod,
    isNodeFullyExperimental,
    type ApiSchema,
    type RpcMethod,
} from "./utils.js";
  • Files reviewed: 5/5 changed files
  • Comments generated: 7

Comment on lines +190 to +191
const defs = (schema.definitions ?? schema.$defs ?? {}) as Record<string, JSONSchema7Definition>;
return { ...defs }
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collectDefinitions currently prefers schema.definitions over schema.$defs and ignores the other when both exist. To reliably support mixed-draft schemas, merge both maps (with a defined precedence on collisions) so referenced types aren’t dropped.

Suggested change
const defs = (schema.definitions ?? schema.$defs ?? {}) as Record<string, JSONSchema7Definition>;
return { ...defs }
const legacyDefinitions = (schema.definitions ?? {}) as Record<string, JSONSchema7Definition>;
const draft2019Definitions = (schema.$defs ?? {}) as Record<string, JSONSchema7Definition>;
return { ...draft2019Definitions, ...legacyDefinitions };

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +98
const combinedSchema: JSONSchema7 = {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
definitions: { ...sharedDefs },
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The combined schema only populates definitions. If an input schema uses $defs and $ref: "#/$defs/...", json-schema-to-typescript won’t be able to resolve those pointers unless the combined schema also includes $defs (or refs are normalized). Consider emitting both definitions and $defs pointing at the shared map, or rewriting $ref paths during preprocessing.

Suggested change
const combinedSchema: JSONSchema7 = {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
definitions: { ...sharedDefs },
const sharedDefinitions: Record<string, unknown> = { ...sharedDefs };
const combinedSchema: JSONSchema7 & { $defs: Record<string, unknown> } = {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
definitions: sharedDefinitions,
$defs: sharedDefinitions,

Copilot uses AI. Check for mistakes.
Comment on lines +260 to +262
const schemaWithDefs: JSONSchema7 = {
...(typeof def === "object" ? (def as JSONSchema7) : {}),
definitions: combinedSchema.definitions,
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During RPC generation, each per-type schema source is created with only definitions. If an API schema uses $defs and $ref: "#/$defs/...", quicktype may fail to resolve those references. Include a $defs field (or normalize refs) alongside definitions when building schemaWithDefs to cover both pointer styles.

Suggested change
const schemaWithDefs: JSONSchema7 = {
...(typeof def === "object" ? (def as JSONSchema7) : {}),
definitions: combinedSchema.definitions,
const schemaWithDefs: JSONSchema7 & { $defs?: JSONSchema7["definitions"] } = {
...(typeof def === "object" ? (def as JSONSchema7) : {}),
definitions: combinedSchema.definitions,
$defs: combinedSchema.definitions,

Copilot uses AI. Check for mistakes.
Comment on lines +825 to 832
// Build a combined schema for quicktype — prefix types to avoid conflicts.
// Include shared definitions from the API schema for $ref resolution.
const sharedDefs = collectDefinitions(schema as Record<string, unknown>);
const combinedSchema: JSONSchema7 = {
$schema: "http://json-schema.org/draft-07/schema#",
definitions: {},
definitions: { ...sharedDefs },
};

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The combined RPC schema only sets definitions. If the API schema’s $ref pointers use #/$defs/..., quicktype may not resolve them unless $defs is also present (or refs are normalized). Consider adding $defs: combinedSchema.definitions (or normalizing $ref) when building the combined schema and per-type sources.

See below for a potential fix:

    const combinedDefinitions: Record<string, JSONSchema7Definition> = { ...sharedDefs };
    const combinedSchema: JSONSchema7 & { $defs?: Record<string, JSONSchema7Definition> } = {
        $schema: "http://json-schema.org/draft-07/schema#",
        definitions: combinedDefinitions,
        $defs: combinedDefinitions,

Copilot uses AI. Check for mistakes.
Comment on lines +415 to +424
if (!nestedClasses.has(className)) {
const refSchema = resolveRef(propSchema.$ref, sessionDefinitions);
if (refSchema) {
if (refSchema.enum && Array.isArray(refSchema.enum)) {
return getOrCreateEnum(className, "", refSchema.enum as string[], enumOutput);
}
nestedClasses.set(className, generateNestedClass(className, refSchema, knownTypes, nestedClasses, enumOutput));
}
}
return isRequired ? className : `${className}?`;
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For $ref to an enum schema, the code returns the enum type name directly, bypassing the isRequired nullability handling. This makes optional enum properties non-nullable. Apply ? when isRequired is false (and keep enum generation/dedup logic intact).

Suggested change
if (!nestedClasses.has(className)) {
const refSchema = resolveRef(propSchema.$ref, sessionDefinitions);
if (refSchema) {
if (refSchema.enum && Array.isArray(refSchema.enum)) {
return getOrCreateEnum(className, "", refSchema.enum as string[], enumOutput);
}
nestedClasses.set(className, generateNestedClass(className, refSchema, knownTypes, nestedClasses, enumOutput));
}
}
return isRequired ? className : `${className}?`;
let resolvedTypeName = className;
if (!nestedClasses.has(className)) {
const refSchema = resolveRef(propSchema.$ref, sessionDefinitions);
if (refSchema) {
if (refSchema.enum && Array.isArray(refSchema.enum)) {
resolvedTypeName = getOrCreateEnum(className, "", refSchema.enum as string[], enumOutput);
} else {
nestedClasses.set(className, generateNestedClass(className, refSchema, knownTypes, nestedClasses, enumOutput));
}
}
}
return isRequired ? resolvedTypeName : `${resolvedTypeName}?`;

Copilot uses AI. Check for mistakes.
Comment on lines 525 to 528
function generateSessionEventsCode(schema: JSONSchema7): string {
generatedEnums.clear();
sessionDefinitions = schema.definitions as Record<string, JSONSchema7Definition> || {};
const variants = extractEventVariants(schema);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sessionDefinitions is populated from schema.definitions only. If the session-events schema uses $defs (or if $ref paths point to #/$defs/...), $ref resolution in resolveSessionPropertyType will fail. Populate sessionDefinitions via collectDefinitions(...) (or preprocess the schema to normalize $defsdefinitions).

See below for a potential fix:

    sessionDefinitions = collectDefinitions(schema);
    const variants = extractEventVariants(schema);
    const knownTypes = new Map<string, string>();
    const nestedClasses = new Map<string, string>();
    const enumOutput: string[] = [];

    // Extract descriptions for base class properties from the first variant
    const sessionEventDefinition = sessionDefinitions.SessionEvent;
    const firstVariant = typeof sessionEventDefinition === "object" ? (sessionEventDefinition as JSONSchema7)?.anyOf?.[0] : undefined;

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +66
// Normalize $defs → definitions for draft 2019+ compatibility
if ("$defs" in processed && !processed.definitions) {
processed.definitions = (processed as Record<string, unknown>).$defs as Record<
string,
JSONSchema7Definition
>;
delete (processed as Record<string, unknown>).$defs;
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

postProcessSchema converts $defs to definitions but does not update existing $ref pointers (e.g. #/$defs/Foo). If an input schema uses $defs-style refs, codegen will still try to resolve via $defs and fail after $defs is deleted. Either keep $defs alongside definitions, or rewrite $ref strings during post-processing.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants