import Link from "next/link"; import { Code } from "@/components/code"; export const metadata = { title: "OpenAPI Integration | json-render", }; export default function OpenAPIPage() { return (

OpenAPI Integration

Use json-render to generate dynamic forms and UIs from{" "} OpenAPI/Swagger {" "} schemas.

Concept: This page demonstrates how json-render can support OpenAPI schemas. The examples are illustrative and may require adaptation for production use.

Why OpenAPI?

OpenAPI specifications describe your API{"'"}s endpoints, request bodies, and response schemas. By converting OpenAPI schemas to json-render specs, you can:

Example OpenAPI Schema

A typical OpenAPI schema for a request body:

{`{ "openapi": "3.0.0", "paths": { "/users": { "post": { "summary": "Create a new user", "operationId": "createUser", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateUserRequest" } } } } } } }, "components": { "schemas": { "CreateUserRequest": { "type": "object", "required": ["email", "name"], "properties": { "name": { "type": "string", "description": "User's full name", "minLength": 1, "maxLength": 100 }, "email": { "type": "string", "format": "email", "description": "User's email address" }, "age": { "type": "integer", "minimum": 0, "maximum": 150, "description": "User's age" }, "role": { "type": "string", "enum": ["admin", "user", "guest"], "default": "user", "description": "User's role" }, "preferences": { "type": "object", "properties": { "newsletter": { "type": "boolean", "default": false }, "theme": { "type": "string", "enum": ["light", "dark", "system"] } } } } } } } }`}

Define an OpenAPI-to-UI Catalog

Create components that map to OpenAPI data types:

{`import { createCatalog } from '@json-render/core'; import { z } from 'zod'; export const openapiCatalog = createCatalog({ components: { // Form container Form: { description: 'API form container', props: z.object({ operationId: z.string(), endpoint: z.string(), method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']), title: z.string().optional(), description: z.string().optional(), }), }, // Field components mapped to OpenAPI types StringField: { description: 'String input field', props: z.object({ name: z.string(), label: z.string(), description: z.string().optional(), required: z.boolean().optional(), format: z.enum(['text', 'email', 'uri', 'uuid', 'date', 'date-time', 'password']).optional(), minLength: z.number().optional(), maxLength: z.number().optional(), pattern: z.string().optional(), placeholder: z.string().optional(), defaultValue: z.string().optional(), }), }, NumberField: { description: 'Number input field', props: z.object({ name: z.string(), label: z.string(), description: z.string().optional(), required: z.boolean().optional(), type: z.enum(['integer', 'number']).optional(), minimum: z.number().optional(), maximum: z.number().optional(), exclusiveMinimum: z.number().optional(), exclusiveMaximum: z.number().optional(), multipleOf: z.number().optional(), defaultValue: z.number().optional(), }), }, BooleanField: { description: 'Boolean toggle field', props: z.object({ name: z.string(), label: z.string(), description: z.string().optional(), defaultValue: z.boolean().optional(), }), }, EnumField: { description: 'Enum selection field', props: z.object({ name: z.string(), label: z.string(), description: z.string().optional(), required: z.boolean().optional(), options: z.array(z.object({ value: z.string(), label: z.string().optional(), })), defaultValue: z.string().optional(), }), }, ArrayField: { description: 'Array of items', props: z.object({ name: z.string(), label: z.string(), description: z.string().optional(), minItems: z.number().optional(), maxItems: z.number().optional(), uniqueItems: z.boolean().optional(), }), }, ObjectField: { description: 'Nested object group', props: z.object({ name: z.string(), label: z.string(), description: z.string().optional(), collapsible: z.boolean().optional(), }), }, // Response display components ResponseDisplay: { description: 'Displays API response', props: z.object({ status: z.number(), statusText: z.string().optional(), }), }, SchemaTable: { description: 'Displays data matching a schema', props: z.object({ schema: z.string(), data: z.array(z.record(z.unknown())), }), }, }, actions: { submit: { description: 'Submit form to API endpoint', params: z.object({ operationId: z.string(), }), }, reset: { description: 'Reset form to defaults', params: z.object({}), }, }, });`}

Convert OpenAPI Schema to Spec

Transform OpenAPI schemas into json-render specs:

{`interface OpenAPISchema { type?: string; format?: string; enum?: string[]; properties?: Record; items?: OpenAPISchema; required?: string[]; description?: string; minimum?: number; maximum?: number; minLength?: number; maxLength?: number; default?: unknown; } interface SpecElement { key: string; type: string; props: Record; children: string[]; parentKey: string; } function schemaToSpec( schema: OpenAPISchema, name: string, required: string[] = [], parentKey: string = '', elements: Map = new Map(), ): string { const key = parentKey ? \`\${parentKey}-\${name}\` : name; const isRequired = required.includes(name); const label = name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, ' $1'); if (schema.enum) { elements.set(key, { key, type: 'EnumField', props: { name, label, description: schema.description, required: isRequired, options: schema.enum.map(v => ({ value: v, label: v })), defaultValue: schema.default as string, }, children: [], parentKey, }); } else if (schema.type === 'string') { elements.set(key, { key, type: 'StringField', props: { name, label, description: schema.description, required: isRequired, format: schema.format || 'text', minLength: schema.minLength, maxLength: schema.maxLength, defaultValue: schema.default as string, }, children: [], parentKey, }); } else if (schema.type === 'integer' || schema.type === 'number') { elements.set(key, { key, type: 'NumberField', props: { name, label, description: schema.description, required: isRequired, type: schema.type, minimum: schema.minimum, maximum: schema.maximum, defaultValue: schema.default as number, }, children: [], parentKey, }); } else if (schema.type === 'boolean') { elements.set(key, { key, type: 'BooleanField', props: { name, label, description: schema.description, defaultValue: schema.default as boolean, }, children: [], parentKey, }); } else if (schema.type === 'array' && schema.items) { const childKeys: string[] = []; const itemKey = schemaToSpec(schema.items, 'item', [], key, elements); childKeys.push(itemKey); elements.set(key, { key, type: 'ArrayField', props: { name, label, description: schema.description, }, children: childKeys, parentKey, }); } else if (schema.type === 'object' && schema.properties) { const childKeys: string[] = []; for (const [propName, propSchema] of Object.entries(schema.properties)) { const childKey = schemaToSpec( propSchema, propName, schema.required || [], key, elements, ); childKeys.push(childKey); } elements.set(key, { key, type: 'ObjectField', props: { name, label, description: schema.description, }, children: childKeys, parentKey, }); } return key; } // Convert full OpenAPI operation to spec export function operationToSpec( operationId: string, method: string, path: string, schema: OpenAPISchema, title?: string, description?: string, ) { const elements = new Map(); const rootKey = 'form'; const childKeys: string[] = []; if (schema.properties) { for (const [name, propSchema] of Object.entries(schema.properties)) { const childKey = schemaToSpec( propSchema, name, schema.required || [], rootKey, elements, ); childKeys.push(childKey); } } elements.set(rootKey, { key: rootKey, type: 'Form', props: { operationId, endpoint: path, method: method.toUpperCase(), title, description, }, children: childKeys, parentKey: '', }); return { root: rootKey, elements: Object.fromEntries(elements), }; }`}

Build an OpenAPI Form Renderer

{`'use client'; import React, { useState } from 'react'; interface FieldProps { name: string; value: unknown; onChange: (name: string, value: unknown) => void; } const fields: Record> = { StringField: ({ name, label, description, required, format, value, onChange }) => (
{description &&

{description}

} onChange(name, e.target.value)} required={required} />
), NumberField: ({ name, label, description, required, minimum, maximum, value, onChange }) => (
{description &&

{description}

} onChange(name, e.target.value ? parseFloat(e.target.value) : undefined)} required={required} />
), BooleanField: ({ name, label, description, value, onChange }) => (
onChange(name, e.target.checked)} className="mt-1" />
{description &&

{description}

}
), EnumField: ({ name, label, description, required, options, value, onChange }) => (
{description &&

{description}

}
), ObjectField: ({ name, label, description, children }) => (
{label} {description &&

{description}

} {children}
), Form: ({ title, description, endpoint, method, children, onSubmit }) => (
{ e.preventDefault(); onSubmit?.(); }} > {title &&

{title}

} {description &&

{description}

} {children}
), }; interface OpenAPIFormProps { spec: { root: string; elements: Record; }; onSubmit: (data: Record) => void; } export function OpenAPIForm({ spec, onSubmit }: OpenAPIFormProps) { const [formData, setFormData] = useState>({}); const handleChange = (name: string, value: unknown) => { setFormData(prev => ({ ...prev, [name]: value })); }; function renderElement(key: string): React.ReactNode { const element = spec.elements[key]; if (!element) return null; const Field = fields[element.type]; if (!Field) return null; const children = element.children?.map(renderElement); return ( onSubmit(formData)} > {children} ); } return <>{renderElement(spec.root)}; }`}

Usage Example

{`'use client'; import { OpenAPIForm } from './openapi-form'; import { operationToSpec } from './openapi-to-spec'; // Your OpenAPI schema (typically loaded from your API) const createUserSchema = { type: 'object', required: ['email', 'name'], properties: { name: { type: 'string', description: "User's full name" }, email: { type: 'string', format: 'email', description: "User's email" }, age: { type: 'integer', minimum: 0, maximum: 150 }, role: { type: 'string', enum: ['admin', 'user', 'guest'], default: 'user' }, }, }; // Convert to spec const spec = operationToSpec( 'createUser', 'POST', '/api/users', createUserSchema, 'Create User', 'Add a new user to the system', ); export function CreateUserForm() { const handleSubmit = async (data: Record) => { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (response.ok) { console.log('User created!'); } }; return ; }`}

Auto-generating from OpenAPI Document

Load and parse an OpenAPI document to generate forms for all operations:

{`import SwaggerParser from '@apidevtools/swagger-parser'; import { operationToSpec } from './openapi-to-spec'; interface OpenAPIDocument { paths: Record>; components?: { schemas?: Record; }; } export async function loadOpenAPISpecs(specUrl: string) { const api = await SwaggerParser.dereference(specUrl) as OpenAPIDocument; const specs: Record = {}; for (const [path, methods] of Object.entries(api.paths)) { for (const [method, operation] of Object.entries(methods)) { if (!operation.requestBody?.content?.['application/json']?.schema) continue; const schema = operation.requestBody.content['application/json'].schema; const operationId = operation.operationId || \`\${method}_\${path.replace(/\\//g, '_')}\`; specs[operationId] = operationToSpec( operationId, method, path, schema, operation.summary, operation.description, ); } } return specs; } // Usage const specs = await loadOpenAPISpecs('https://api.example.com/openapi.json'); // specs.createUser, specs.updateUser, etc.`}

Next

Learn about{" "} streaming {" "} for progressive UI rendering.

); }