opencode icon indicating copy to clipboard operation
opencode copied to clipboard

[FEATURE]: JSON Schema as Source of Truth for API Contracts

Open TonyMarkham opened this issue 1 week ago • 1 comments

Feature hasn't been suggested before.

  • [x] I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Proposal: JSON Schema as Source of Truth for API Contracts

Summary

Move API schema definitions from inline TypeScript/Zod code to formal JSON Schema files. Generate TypeScript types and Zod validators from these schemas. This improves API discoverability, enables client SDK generation, and provides versioning for API contracts.

Problem Statement

Currently, OpenCode's API contracts are defined inline within TypeScript source code using Zod:

// packages/opencode/src/provider/provider.ts
export const Info = z.object({
  id: z.string(),
  name: z.string(),
  source: z.enum(["env", "config", "custom", "api"]),
  // ... scattered across 500+ lines
})

This creates several issues:

  1. No single source of truth - Schema definitions are scattered across multiple files
  2. Hard to discover - External clients must read TypeScript source to understand the API
  3. No versioning - Breaking changes have no semantic versioning
  4. No client generation - Each client (desktop, SDK, integration) reverse-engineers the API
  5. Poor documentation - Schema docs are inline comments, not structured
  6. Difficult to validate - No way to validate JSON payloads against schemas at build time

Real-World Impact

I'm building a Tauri desktop client for OpenCode and had to:

  • Read through 1000+ lines of TypeScript to understand Provider.Info schema
  • Document commit hashes and line numbers as "source of truth" references
  • Manually translate Zod schemas to Protocol Buffers for type safety
  • Risk breaking changes with every OpenCode update

This shouldn't be necessary for a modern API.

Proposed Solution

1. Create schema/ Directory with JSON Schema Definitions

opencode/
├── schema/
│   ├── provider.schema.json       # Source of truth
│   ├── model.schema.json
│   ├── session.schema.json
│   ├── message.schema.json
│   ├── tool.schema.json
│   ├── agent.schema.json
│   └── auth.schema.json

2. Example: Provider Schema

schema/provider.schema.json:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://opencode.ai/schemas/v1/provider.json",
  "title": "Provider",
  "description": "AI model provider configuration and metadata",
  "type": "object",
  "required": ["id", "name", "source", "models"],
  "properties": {
    "id": {
      "type": "string",
      "description": "Unique provider identifier",
      "examples": ["anthropic", "openai", "google"]
    },
    "name": {
      "type": "string",
      "description": "Human-readable provider name",
      "examples": ["Anthropic", "OpenAI"]
    },
    "source": {
      "type": "string",
      "enum": ["env", "config", "custom", "api"],
      "description": "How the provider was configured"
    },
    "env": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Environment variables used for authentication",
      "examples": [["ANTHROPIC_API_KEY"]]
    },
    "options": {
      "$ref": "#/definitions/ProviderOptions"
    },
    "models": {
      "type": "object",
      "additionalProperties": {
        "$ref": "./model.schema.json"
      }
    }
  },
  "definitions": {
    "ProviderOptions": {
      "type": "object",
      "properties": {
        "baseURL": { 
          "type": "string", 
          "format": "uri",
          "description": "API endpoint override"
        },
        "apiKey": { 
          "type": "string",
          "description": "API authentication key"
        },
        "headers": {
          "type": "object",
          "additionalProperties": { "type": "string" },
          "description": "Custom HTTP headers"
        },
        "timeout": { 
          "type": "integer", 
          "minimum": 0,
          "description": "Request timeout in milliseconds"
        }
      }
    }
  }
}

3. Generate TypeScript Types and Zod Validators

Build process:

# Generate TypeScript interfaces from JSON Schema
json-schema-to-typescript schema/*.json --output src/generated/types/

# Generate Zod validators from JSON Schema  
json-schema-to-zod schema/*.json --output src/generated/validators/

Auto-generated types (src/generated/types/provider.ts):

export interface Provider {
  id: string;
  name: string;
  source: "env" | "config" | "custom" | "api";
  env?: string[];
  options?: ProviderOptions;
  models: Record<string, Model>;
}

export interface ProviderOptions {
  baseURL?: string;
  apiKey?: string;
  headers?: Record<string, string>;
  timeout?: number;
}

Auto-generated validators (src/generated/validators/provider.ts):

import { z } from "zod";

export const ProviderOptionsSchema = z.object({
  baseURL: z.string().url().optional(),
  apiKey: z.string().optional(),
  headers: z.record(z.string()).optional(),
  timeout: z.number().int().min(0).optional(),
});

export const ProviderSchema = z.object({
  id: z.string(),
  name: z.string(),
  source: z.enum(["env", "config", "custom", "api"]),
  env: z.array(z.string()).optional(),
  options: ProviderOptionsSchema.optional(),
  models: z.record(ModelSchema),
});

4. Update Source Code to Use Generated Types

Before:

// packages/opencode/src/provider/provider.ts
export const Info = z.object({
  id: z.string(),
  name: z.string(),
  // ... 50 lines of schema definition
})
export type Info = z.infer<typeof Info>

After:

// packages/opencode/src/provider/provider.ts
import type { Provider } from "../generated/types/provider"
import { ProviderSchema } from "../generated/validators/provider"

// Business logic uses generated types
export async function list(): Promise<Record<string, Provider>> {
  // ... implementation
}

// API validation uses generated validators
export function validate(data: unknown): Provider {
  return ProviderSchema.parse(data)
}

Zero runtime behavior changes. Pure refactor.

Benefits

For OpenCode Maintainers

  1. Single source of truth - All schemas in one place (schema/ directory)
  2. Better documentation - Structured, versionable schema docs
  3. Automatic validation - Catch schema bugs at build time
  4. VSCode integration - Auto-complete and validation in JSON Schema files
  5. No runtime changes - Generated code works exactly like hand-written code
  6. Industry standard - JSON Schema is used by OpenAPI, VSCode, npm, etc.

For External Developers

  1. API discovery - Read schemas without diving into TypeScript source
  2. Type-safe clients - Generate SDKs in any language (TypeScript, Python, Rust, C#)
  3. Version tracking - Know when breaking changes occur
  4. Better docs - Auto-generate API documentation from schemas
  5. Validation tools - Validate payloads against published schemas

For the Ecosystem

  1. Desktop clients - Can generate type-safe gRPC/REST clients
  2. SDK packages - Auto-generate @opencode/sdk-python, etc.
  3. IDE extensions - Better autocomplete and validation
  4. Testing tools - Schema-based fuzz testing and validation
  5. Documentation sites - Auto-generated API reference docs

Implementation Plan

Phase 1: Proof of Concept (Week 1)

  • [ ] Create schema/provider.schema.json
  • [ ] Set up build tooling (json-schema-to-typescript, json-schema-to-zod)
  • [ ] Generate types and validators
  • [ ] Update packages/opencode/src/provider/provider.ts to use generated code
  • [ ] Verify all existing tests pass (zero behavior changes)
  • [ ] Create PR with PoC

Phase 2: Core Schemas (Weeks 2-4)

Migrate one schema per PR:

  • [ ] model.schema.json - Model metadata, capabilities, options
  • [ ] session.schema.json - Session/conversation management
  • [ ] message.schema.json - User/assistant messages
  • [ ] tool.schema.json - Tool execution state
  • [ ] agent.schema.json - Agent definitions
  • [ ] auth.schema.json - Authentication

Phase 3: Tooling & Versioning (Week 5)

  • [ ] Add semantic versioning to schemas (v1.0.0, v1.1.0)
  • [ ] Set up schema validation in CI (catch breaking changes)
  • [ ] Document schema contribution guidelines
  • [ ] Add npm run schema:validate command

Phase 4: Documentation & SDKs (Week 6+)

  • [ ] Generate OpenAPI spec from JSON Schemas
  • [ ] Auto-generate API documentation site
  • [ ] Publish @opencode/schemas package to npm
  • [ ] Create SDK generation guide for external developers

Migration Strategy

Gradual, Non-Breaking

  1. Add schemas alongside existing code - No immediate changes required
  2. Generate code during build - Transparent to contributors
  3. Migrate one domain at a time - Provider → Model → Session → etc.
  4. Keep Zod validators working - Auto-generated, zero behavior changes
  5. Tests prove equivalence - All existing tests must pass

Backward Compatibility

  • Existing code continues to work during migration
  • Generated Zod schemas are functionally identical to hand-written ones
  • No API changes, no runtime changes
  • Contributors can still modify schemas (just edit JSON instead of TypeScript)

Tooling

Required Dependencies (dev only)

{
  "devDependencies": {
    "json-schema-to-typescript": "^13.1.0",
    "json-schema-to-zod": "^2.0.0",
    "ajv": "^8.12.0",
    "ajv-cli": "^5.0.0"
  }
}

Build Scripts

{
  "scripts": {
    "schema:types": "json-schema-to-typescript schema/*.json -o src/generated/types/",
    "schema:zod": "json-schema-to-zod schema/*.json -o src/generated/validators/",
    "schema:validate": "ajv validate -s schema/**/*.json",
    "schema:docs": "generate-schema-doc schema/ -o docs/api/",
    "prebuild": "npm run schema:types && npm run schema:zod"
  }
}

Auto-runs before every build. No manual intervention needed.

Examples of This Pattern in the Wild

Projects successfully using JSON Schema as source of truth:

  • VSCode - Extension API schemas
  • npm - package.json schema
  • GitHub Actions - Workflow schema
  • Docker Compose - docker-compose.yml schema
  • Kubernetes - CRD schemas
  • OpenAPI/Swagger - Built on JSON Schema

This is an industry-standard approach, not experimental.

FAQ

Q: Won't this make contributions harder?

A: No. Contributors edit JSON instead of TypeScript. Both are human-readable. JSON Schema has better VSCode support (auto-complete, validation).

Q: What about runtime performance?

A: Zero impact. Generated Zod validators are identical to hand-written ones. No additional runtime overhead.

Q: Do we need to rewrite everything at once?

A: No. Migrate incrementally. Old code and new code coexist during transition.

Q: What if we need to change a schema?

A: Edit the JSON Schema file, run build, generated code updates automatically. Simpler than editing scattered TypeScript files.

Q: Why not just use TypeScript types?

A: TypeScript types don't exist at runtime. We need runtime validation (Zod). Also, TypeScript types aren't consumable by other languages (Python, Rust, C# clients).

Q: Why not Protocol Buffers?

A: Protobuf has a steep learning curve and "not JavaScript-native" stigma. JSON Schema is widely understood, used by OpenAPI, and has mature JS tooling.

Alternatives Considered

Alternative 1: Keep Current Approach

Pros:

  • No migration effort
  • Works today

Cons:

  • External clients struggle
  • No API versioning
  • Poor discoverability
  • Doesn't scale to ecosystem

Alternative 2: Protocol Buffers

Pros:

  • Strong typing
  • Efficient serialization
  • Multi-language support

Cons:

  • Steep learning curve
  • Not JavaScript-native
  • Resistance from JS community
  • Adds build complexity (protoc)

Alternative 3: OpenAPI Only

Pros:

  • Good for HTTP APIs
  • Auto-generates docs

Cons:

  • Doesn't cover internal models
  • Still need runtime validators
  • Harder to version individual schemas

Decision: JSON Schema is the best balance of power, simplicity, and JavaScript-native tooling.

Call to Action

I'm volunteering to do the implementation work:

  1. Create PoC PR - Provider schema migration (1-2 days)
  2. Gather feedback - Address concerns, iterate on approach
  3. Incremental migration - One schema per PR (1-2 weeks total)
  4. Documentation - Contribution guide, schema versioning docs

I believe this will make OpenCode's API more accessible and enable a healthier ecosystem of clients, SDKs, and integrations.

Related Work

I've already done significant schema reverse-engineering for a Tauri desktop client:

  • Documented all schemas with commit hashes and line numbers
  • Created Protocol Buffer definitions for type safety
  • Identified discrepancies and undocumented fields

This proposal builds on that work and makes it available to the entire community.


Would the maintainers be open to this approach? I'm happy to answer questions and create a PoC PR if there's interest.

TonyMarkham avatar Jan 04 '26 22:01 TonyMarkham