Skip to content

refactor(schemas): Replace discriminated unions with flat schemas for Claude API compatibility #29

@polaz

Description

@polaz

Problem

Claude/Anthropic API returns error when tools use oneOf, allOf, or anyOf at the top level of input schemas:

API Error: 400 {"type":"error","error":{"type":"invalid_request_error",
"message":"tools.19.custom.input_schema: input_schema does not support oneOf, allOf, or anyOf at the top level"}}

Zod's z.discriminatedUnion() compiles to JSON Schema with oneOf at the root level, making our CQRS tools incompatible with Claude API.

Affected Schemas (18 total)

Core Tools

Schema File Actions
BrowseProjectsSchema src/entities/core/schema-readonly.ts search, list, get
BrowseNamespacesSchema src/entities/core/schema-readonly.ts list, get, verify
BrowseCommitsSchema src/entities/core/schema-readonly.ts list, get, diff
BrowseEventsSchema src/entities/core/schema-readonly.ts user, project
ManageRepositorySchema src/entities/core/schema.ts create, fork
ManageTodosSchema src/entities/core/schema.ts mark_done, mark_all_done, restore

MRS Tools

Schema File Actions
BrowseMergeRequestsSchema src/entities/mrs/schema-readonly.ts list, get, diffs, compare
BrowseMrDiscussionsSchema src/entities/mrs/schema-readonly.ts list, drafts, draft
ManageMergeRequestSchema src/entities/mrs/schema.ts create, update, merge
ManageMrDiscussionSchema src/entities/mrs/schema.ts comment, thread, reply, update
ManageDraftNotesSchema src/entities/mrs/schema.ts create, update, publish, publish_all, delete

Files Tools

Schema File Actions
BrowseFilesSchema src/entities/files/schema-readonly.ts tree, content
ManageFilesSchema src/entities/files/schema.ts single, batch, upload

Pipelines Tools

Schema File Actions
BrowsePipelinesSchema src/entities/pipelines/schema-readonly.ts list, get, jobs, triggers, job, logs
ManagePipelineSchema src/entities/pipelines/schema.ts create, retry, cancel
ManagePipelineJobSchema src/entities/pipelines/schema.ts play, retry, cancel

Work Items Tools

Schema File Actions
BrowseWorkItemsSchema src/entities/workitems/schema-readonly.ts list, get
ManageWorkItemSchema src/entities/workitems/schema.ts create, update, delete

Solution

Replace z.discriminatedUnion() with flat z.object() schemas using .refine() for conditional validation:

// BEFORE (generates oneOf at top level - INCOMPATIBLE)
const Schema = z.discriminatedUnion("action", [
  z.object({ action: z.literal("list"), project_id: z.string() }),
  z.object({ action: z.literal("get"), project_id: z.string(), sha: z.string() }),
]);

// AFTER (flat object - COMPATIBLE)
const Schema = z.object({
  action: z.enum(["list", "get"]),
  project_id: z.string(),
  sha: z.string().optional(),
}).refine(
  (data) => data.action !== "get" || data.sha !== undefined,
  { message: "sha is required for 'get' action", path: ["sha"] }
);

Acceptance Criteria

  • All 18 schemas refactored to flat object pattern
  • Runtime validation via `.refine()` for action-specific required fields
  • TypeScript types still correctly infer required fields per action
  • All existing tests pass
  • JSON Schema output has no `oneOf`/`allOf`/`anyOf` at top level
  • Update CLAUDE.md with "AI-Compatible Schema" pattern documentation

Related Issues

This pattern MUST be applied to future CQRS consolidations:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions