Skip to main content
Glama
response-schema.ts7.72 kB
import type { LogLayer } from 'loglayer'; import type Oas from 'oas'; import type { HttpMethods } from 'oas/types'; import { z } from 'zod'; import { jsonSchemaObjectToZodRawShape } from 'zod-from-json-schema'; import type { JSONSchema } from 'zod-from-json-schema'; import type { PathOperation } from '../client.ts'; export interface OpPointer { path: string; method: HttpMethods; } /** * Handles extraction and management of response schemas from OpenAPI specifications */ export class ResponseSchemaExtractor { static from(oas: Oas, pointer: OpPointer, log: LogLayer): ResponseSchemaExtractor { const op = oas.getOperation(pointer.path, pointer.method); return new ResponseSchemaExtractor(op, log); } static fromOp(op: PathOperation, log: LogLayer): ResponseSchemaExtractor { return new ResponseSchemaExtractor(op, log); } readonly #op: PathOperation; readonly #log: LogLayer; readonly #cache = new Map<string, JSONSchema | null>(); #statusCodes: string[] = []; /** * Creates a new ResponseSchemaExtractor * * @param oas - OpenAPI specification object with path and method information * @param log - Logger instance for debugging and error reporting */ private constructor(op: PathOperation, log: LogLayer) { this.#op = op; this.#log = log; // Initialize status codes cache this.#statusCodes = this.#op.getResponseStatusCodes(); } /** * Retrieves the response schema for a specific status code. * Results are cached for better performance. * If the specific status code is not found, it falls back to 'default'. * * @param code - HTTP status code (e.g., '200', '404') or 'default' * @returns JSON Schema for the response or null if not found */ getSchema(code: string): JSONSchema | null { // Check cache first if (this.#cache.has(code)) { return this.#cache.get(code) ?? null; } try { // Try to get the schema for the specific status code let schema = this.#extractSchema(code); // If not found and it's not 'default', try the default schema if (!schema && code !== 'default') { schema = this.#extractSchema('default'); } // Cache the result (even if null) this.#cache.set(code, schema); return schema; } catch (error) { this.#log.error( `Error getting response schema for status code ${code}:`, error instanceof Error ? error.message : String(error), ); return null; } } /** * Get all response schemas defined for this operation * * This getter returns all available response schemas indexed by their status codes. * The result is cached for better performance on subsequent calls. * * @returns Record of status codes to JSON Schema objects * * @example * ```typescript * const schemas = operation.responseSchemas; * // Access individual schemas by status code * const okSchema = schemas['200']; * const errorSchema = schemas['400']; * ``` */ get schemas(): Record<string, JSONSchema> { return Object.fromEntries( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.statusCodes.map((code) => [code, this.getSchema(code)!]), ); } /** * Extract response schema for a specific status code from the OpenAPI specification * * This method retrieves the response schema for the specified status code. * It handles errors and returns null if the schema is not found. * * @param statusCode - HTTP status code (e.g., '200', '404') or 'default' * @returns JSON Schema for the response or null if not found */ #extractSchema(statusCode: string): JSONSchema | null { try { const response = this.#op.getResponseByStatusCode(statusCode); if (typeof response === 'boolean') { this.#log.debug(`No response found for status code ${statusCode}`); return null; } const content = response.content; if (!content) { this.#log.debug(`No content found for status code ${statusCode}`); return null; } const hasJson = 'application/json' in content; // Get all available content types const [firstContentType] = Object.keys(content); if (firstContentType === undefined) { this.#log.debug(`No content types found for status code ${statusCode}`); return null; } // Start with the first content type as default let selectedContentType = firstContentType; if (hasJson) { // Priority 1: application/json selectedContentType = 'application/json'; } else { // Priority 2: JSON-compatible types const jsonCompatible = Object.keys(content).find( (type) => type.startsWith('application/') && (type.endsWith('+json') || type.includes('json')), ); if (jsonCompatible) { selectedContentType = jsonCompatible; } // Otherwise keep the default (first content type) } // TypeScript guard to ensure selectedContentType is a valid property key // This is redundant given our selection logic but satisfies TypeScript's type checker if (typeof selectedContentType !== 'string' || !(selectedContentType in content)) { this.#log.debug( `Selected content type ${selectedContentType} not found in response for status code ${statusCode}`, ); return null; } // Now we can safely access contentObj with the selected content type // TypeScript now knows selectedContentType is a valid key in contentObj const contentTypeObj = content[selectedContentType]; // Use optional chaining for more concise property access const schema = contentTypeObj?.schema; if (!schema) { this.#log.debug( `No schema found for content type ${selectedContentType} and status code ${statusCode}`, ); return null; } return schema as JSONSchema; } catch (error) { this.#log.error( `Error extracting response schema for status code ${statusCode}`, error instanceof Error ? error.message : String(error), ); return null; } } /** * Gets a list of all available response status codes from the OpenAPI specification. * Results are cached for better performance. * * @returns Array of status codes as strings */ get statusCodes(): string[] { return [...this.#statusCodes]; } } /** * Get all response schemas converted to Zod schemas for runtime validation * * This getter converts all available JSON Schemas to Zod schemas for runtime * type validation. The result is cached for better performance. * * @returns Record of status codes to Zod schema objects * * @example * ```typescript * const schemas = operation.zodResponseSchemas; * * // Validate a response against the schema * try { * const validatedData = schemas['200'].parse(responseData); * } catch (error) { * console.error('Response validation failed:', error); * } * ``` */ export function zodResponseSchemas( schemas: Record<string, JSONSchema>, app: { log: LogLayer }, ): Record<string, z.ZodObject<z.ZodRawShape>> { const result: Record<string, z.ZodObject<z.ZodRawShape>> = {}; // Convert each JSONSchema to a Zod schema for (const [code, schema] of Object.entries(schemas)) { try { const zodSchema = z.object(jsonSchemaObjectToZodRawShape(schema)); result[code] = zodSchema; } catch (error) { app.log.error(`Error converting response schema for status ${code} to Zod:`, String(error)); // Skip this schema if conversion fails } } return result; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wycats/mcpify'

If you have feedback or need assistance with the MCP directory API, please join our Discord server