// Name: JSON Schema Extractor // Description: Analyze pasted JSON to generate a readable schema with type detection and sample previews, and copy as YAML/JSON/raw. // Author: dallascrilley // GitHub: dallascrilley import "@johnlindquist/kit" import YAML from "yaml" type Kind = | "object" | "array" | "string" | "number" | "integer" | "boolean" | "null" | "email" | "url" | "datetime" | "numeric-string" type NodeSchema = { type: Kind path: string sample?: any properties?: Record<string, NodeSchema> items?: NodeSchema | NodeSchema[] required?: string[] } type DebugEvent = { t: number; msg: string } const debugLog: DebugEvent[] = [] const t0 = Date.now() const logEvent = (msg: string) => debugLog.push({ t: Date.now() - t0, msg }) const MAX_STRING = 100 const MAX_PREVIEW_KEYS = 3 const MAX_PREVIEW_ITEMS = 3 const truncateString = (s: string, max = MAX_STRING) => { if (s.length <= max) return s return s.slice(0, max) + "..." } const previewValue = (v: any): any => { if (v === null || v === undefined) return v if (typeof v === "string") return truncateString(v) if (typeof v === "number" || typeof v === "boolean") return v if (Array.isArray(v)) return previewArray(v) if (typeof v === "object") return previewObject(v) return v } const previewArray = (arr: any[]): any[] => { const slice = arr.slice(0, MAX_PREVIEW_ITEMS).map(previewValue) if (arr.length > MAX_PREVIEW_ITEMS) { slice.push(`… +${arr.length - MAX_PREVIEW_ITEMS} more`) } return slice } const previewObject = (obj: Record<string, any>) => { const keys = Object.keys(obj) const firstKeys = keys.slice(0, MAX_PREVIEW_KEYS) const out: Record<string, any> = {} for (const k of firstKeys) out[k] = previewValue(obj[k]) if (keys.length > MAX_PREVIEW_KEYS) { out["…"] = `${keys.length - MAX_PREVIEW_KEYS} more keys` } return out } const isISODateTime = (s: string) => { // reasonable ISO 8601 check // examples: 2020-01-01T12:34:56Z, 2020-01-01T12:34:56.789Z, 2020-01-01T12:34:56+01:00 const iso = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+\-]\d{2}:\d{2})?$/ if (!iso.test(s)) return false const d = new Date(s) return !Number.isNaN(d.getTime()) } const isEmail = (s: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) const isURL = (s: string) => { try { // Allow http/https only to avoid classifying file:, mailto:, etc. const u = new URL(s) return u.protocol === "http:" || u.protocol === "https:" } catch { return false } } const isNumericString = (s: string) => /^[+-]?\d+(\.\d+)?$/.test(s) const detectStringKind = (s: string): Kind => { if (isEmail(s)) return "email" if (isURL(s)) return "url" if (isISODateTime(s)) return "datetime" if (isNumericString(s)) return "numeric-string" return "string" } const analyze = (value: any, path: string = "$"): NodeSchema => { if (value === null) { return { type: "null", path, sample: null } } const t = typeof value if (t === "string") { return { type: detectStringKind(value), path, sample: truncateString(value) } } if (t === "number") { return { type: Number.isInteger(value) ? "integer" : "number", path, sample: value, } } if (t === "boolean") { return { type: "boolean", path, sample: value } } if (Array.isArray(value)) { const items = value.slice(0, MAX_PREVIEW_ITEMS) const analyzed = items.map((v, i) => analyze(v, `${path}[${i}]`)) // Deduplicate item kinds by a signature const sig = (n: NodeSchema) => n.type + (n.type === "object" ? JSON.stringify(Object.keys(n.properties || {}).sort()) : "") const dedupMap = new Map<string, NodeSchema>() for (const a of analyzed) dedupMap.set(sig(a), a) const unique = [...dedupMap.values()] const node: NodeSchema = { type: "array", path, sample: previewArray(value), } if (unique.length === 1) { node.items = unique[0] } else if (unique.length > 1) { node.items = unique } else { node.items = { type: "null", path: `${path}[*]`, sample: null } } return node } if (t === "object") { const entries = Object.entries(value