type JsonSchema = { type?: string | string[] title?: string description?: string tags?: string[] 'x-tags'?: string[] properties?: Record items?: JsonSchema | JsonSchema[] additionalProperties?: JsonSchema | boolean enum?: unknown[] const?: unknown default?: unknown anyOf?: JsonSchema[] oneOf?: JsonSchema[] allOf?: JsonSchema[] nullable?: boolean minimum?: number maximum?: number pattern?: string } export type { JsonSchema } /** Resolve the primary type from a schema node */ export function schemaType(schema: JsonSchema | undefined): string | undefined { if (!schema) return undefined if (Array.isArray(schema.type)) { const filtered = schema.type.filter(t => t !== 'null') return filtered[0] ?? schema.type[0] } if (schema.type) return schema.type if (schema.properties || schema.additionalProperties) return 'object' return undefined } /** Normalize union schemas (anyOf/oneOf) into a simpler form */ export function normalizeSchema(schema: JsonSchema): JsonSchema { if (!schema.anyOf && !schema.oneOf) return schema const union = schema.anyOf ?? schema.oneOf ?? [] const literals: unknown[] = [] const remaining: JsonSchema[] = [] let nullable = false for (const entry of union) { if (!entry || typeof entry !== 'object') continue if (Array.isArray(entry.enum)) { for (const v of entry.enum) { if (v == null) { nullable = true; continue } if (!literals.some(ex => Object.is(ex, v))) literals.push(v) } continue } if ('const' in entry) { if (entry.const == null) { nullable = true; continue } literals.push(entry.const) continue } if (schemaType(entry) === 'null') { nullable = true; continue } remaining.push(entry) } if (literals.length > 0 && remaining.length === 0) { return { ...schema, enum: literals, nullable, anyOf: undefined, oneOf: undefined } } if (remaining.length === 1) { return { ...remaining[0], nullable, anyOf: undefined, oneOf: undefined, title: schema.title, description: schema.description } } return schema } /** Infer a field type string from a raw config value (schema-less fallback) */ export function inferFieldType(value: unknown): string { if (value == null) return 'string' if (typeof value === 'boolean') return 'boolean' if (typeof value === 'number') return 'number' if (typeof value === 'string') return 'string' if (Array.isArray(value)) return 'array' if (typeof value === 'object') return 'object' return 'string' } /** Collect all tags from a schema tree */ export function extractSchemaTags(schema: JsonSchema): string[] { const tags = new Set() function walk(s: JsonSchema) { for (const t of (s['x-tags'] ?? s.tags ?? [])) { if (typeof t === 'string') tags.add(t.toLowerCase()) } if (s.properties) { for (const child of Object.values(s.properties)) walk(child) } if (s.items && !Array.isArray(s.items)) walk(s.items) } walk(schema) return [...tags] }