JSON Schema Editor

PreviousNext

A visual editor for creating and editing JSON Schema definitions with support for nested objects, arrays, enums, and property descriptions.

Docs
simple-aiui

Preview

Loading preview…
./src/registry/ui/json-schema-editor.tsx
"use client";

import type { JSONSchema7 } from "ai";
import { Asterisk, Brackets, Plus, Save, Trash2 } from "lucide-react";
import { nanoid } from "nanoid";
import { type ComponentProps, useState } from "react";
import { Button } from "@/components/ui/button";
import {
	Dialog,
	DialogContent,
	DialogDescription,
	DialogHeader,
	DialogTitle,
	DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
	Select,
	SelectContent,
	SelectItem,
	SelectTrigger,
	SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";

const MAX_NESTING_LEVEL = 5;

const SCHEMA_TYPES = [
	"string",
	"number",
	"integer",
	"boolean",
	"enum",
	"object",
] as const;

export type JsonSchemaEditorJSONSchemaType = (typeof SCHEMA_TYPES)[number];

export interface JsonSchemaEditorInternalSchemaProperty {
	id: string;
	type: JsonSchemaEditorJSONSchemaType;
	isArray?: boolean;
	isRequired?: boolean;
	enumValues?: string[];
	properties?: Record<string, JsonSchemaEditorInternalSchemaProperty>;
	description?: string;
}

export interface JsonSchemaEditorInternalSchema {
	type: "object";
	properties: Record<string, JsonSchemaEditorInternalSchemaProperty>;
	required?: string[];
	additionalProperties?: boolean;
	description?: string;
}

/**
 * Parse a JSON Schema property into an internal representation
 * Shared utility for both editor and preview components
 */
function parseJSONSchemaProperty(prop: Record<string, unknown>): {
	type: string;
	isArray: boolean;
	description?: string;
	properties?: Record<string, ReturnType<typeof parseJSONSchemaProperty>>;
	enumValues?: string[];
} {
	const result: {
		type: string;
		isArray: boolean;
		description?: string;
		properties?: Record<string, ReturnType<typeof parseJSONSchemaProperty>>;
		enumValues?: string[];
	} = {
		type: "string",
		isArray: false,
	};

	if (prop.type === "array" && prop.items && typeof prop.items === "object") {
		result.isArray = true;
		const items = prop.items as Record<string, unknown>;

		if (typeof items.type === "string") {
			result.type = items.type;
		}

		if (items.enum && Array.isArray(items.enum)) {
			result.type = "enum";
			result.enumValues = items.enum.map(String);
		}

		if (
			items.type === "object" &&
			items.properties &&
			typeof items.properties === "object"
		) {
			result.type = "object";
			result.properties = {};

			for (const [nestedName, nestedProp] of Object.entries(
				items.properties as Record<string, unknown>,
			)) {
				if (typeof nestedProp === "object" && nestedProp !== null) {
					result.properties[nestedName] = parseJSONSchemaProperty(
						nestedProp as Record<string, unknown>,
					);
				}
			}
		}
	} else {
		if (typeof prop.type === "string") {
			result.type = prop.type;
		}

		if (prop.enum && Array.isArray(prop.enum)) {
			result.type = "enum";
			result.enumValues = prop.enum.map(String);
		}

		if (
			prop.type === "object" &&
			prop.properties &&
			typeof prop.properties === "object"
		) {
			result.properties = {};

			for (const [nestedName, nestedProp] of Object.entries(
				prop.properties as Record<string, unknown>,
			)) {
				if (typeof nestedProp === "object" && nestedProp !== null) {
					result.properties[nestedName] = parseJSONSchemaProperty(
						nestedProp as Record<string, unknown>,
					);
				}
			}
		}
	}

	if (prop.description && typeof prop.description === "string") {
		result.description = prop.description;
	}

	return result;
}

function convertToStandardJSONSchema(
	schema: JsonSchemaEditorInternalSchema,
): JSONSchema7 {
	const convertProperty = (
		prop: JsonSchemaEditorInternalSchemaProperty,
	): Record<string, unknown> => {
		const converted: Record<string, unknown> = {};

		if (prop.isArray) {
			converted.type = "array";
			const itemType: Record<string, unknown> = { type: prop.type };

			if (prop.type === "object" && prop.properties) {
				const nestedProperties: Record<string, unknown> = {};
				const nestedRequired: string[] = [];

				for (const [name, nestedProp] of Object.entries(
					prop.properties,
				)) {
					nestedProperties[name] = convertProperty(nestedProp);
					if (nestedProp.isRequired) {
						nestedRequired.push(name);
					}
				}

				itemType.properties = nestedProperties;
				if (nestedRequired.length > 0) {
					itemType.required = nestedRequired;
				}
			} else if (prop.type === "enum" && prop.enumValues) {
				itemType.enum = prop.enumValues;
			}

			converted.items = itemType;
		} else {
			converted.type = prop.type;

			if (prop.type === "enum" && prop.enumValues) {
				converted.enum = prop.enumValues;
				delete converted.type;
			}

			if (prop.type === "object" && prop.properties) {
				const nestedProperties: Record<string, unknown> = {};
				const nestedRequired: string[] = [];

				for (const [name, nestedProp] of Object.entries(
					prop.properties,
				)) {
					nestedProperties[name] = convertProperty(nestedProp);
					if (nestedProp.isRequired) {
						nestedRequired.push(name);
					}
				}

				converted.properties = nestedProperties;
				if (nestedRequired.length > 0) {
					converted.required = nestedRequired;
				}
			}
		}

		if (prop.description) {
			converted.description = prop.description;
		}

		return converted;
	};

	const result: Record<string, unknown> = {
		type: "object",
		properties: {},
	};

	const properties: Record<string, unknown> = {};
	const required: string[] = [];

	for (const [name, prop] of Object.entries(schema.properties || {})) {
		properties[name] = convertProperty(prop);
		if (prop.isRequired) {
			required.push(name);
		}
	}

	result.properties = properties;
	if (required.length > 0) {
		result.required = required;
	}

	if (schema.description) {
		result.description = schema.description;
	}

	return result;
}

function convertFromStandardJSONSchema(
	schema: JSONSchema7 | null,
): JsonSchemaEditorInternalSchema {
	if (!schema || typeof schema !== "object") {
		return {
			type: "object",
			properties: {},
		};
	}

	const convertProperty = (
		prop: Record<string, unknown>,
		isRequired = false,
	): JsonSchemaEditorInternalSchemaProperty => {
		const parsed = parseJSONSchemaProperty(prop);

		const internal: JsonSchemaEditorInternalSchemaProperty = {
			id: nanoid(),
			type: parsed.type as JsonSchemaEditorJSONSchemaType,
			isArray: parsed.isArray,
			isRequired,
			description: parsed.description,
			enumValues: parsed.enumValues,
		};

		if (parsed.properties) {
			internal.properties = {};
			const requiredArray = Array.isArray(prop.required)
				? prop.required
				: [];

			for (const [name, nestedParsed] of Object.entries(
				parsed.properties,
			)) {
				internal.properties[name] = convertProperty(
					nestedParsed as unknown as Record<string, unknown>,
					requiredArray.includes(name),
				);
			}
		}

		return internal;
	};

	const result: JsonSchemaEditorInternalSchema = {
		type: "object",
		properties: {},
	};

	if (schema.properties && typeof schema.properties === "object") {
		const requiredArray = Array.isArray(schema.required)
			? schema.required
			: [];

		for (const [name, prop] of Object.entries(
			schema.properties as Record<string, unknown>,
		)) {
			if (typeof prop === "object" && prop !== null) {
				const converted = convertProperty(
					prop as Record<string, unknown>,
				);
				converted.isRequired = requiredArray.includes(name);
				result.properties[name] = converted;
			}
		}
	}

	if (schema.description && typeof schema.description === "string") {
		result.description = schema.description;
	}

	return result;
}

// ============================================================================
// Schema Editor Components
// ============================================================================

interface JsonSchemaEditorProps {
	initialSchema: JSONSchema7;
	onSave: (schema: JSONSchema7) => void;
	onClose: () => void;
	className?: string;
}

export function JsonSchemaEditor({
	initialSchema,
	onSave,
	onClose,
	className,
}: JsonSchemaEditorProps) {
	const [internalSchema, setInternalSchema] =
		useState<JsonSchemaEditorInternalSchema>(() =>
			convertFromStandardJSONSchema(initialSchema),
		);

	const [schemaDescription, setSchemaDescription] = useState<string>(
		initialSchema?.description || "",
	);

	const handleInternalChange = (updated: JsonSchemaEditorInternalSchema) => {
		setInternalSchema(updated);
	};

	const handleSave = () => {
		const standardSchema = convertToStandardJSONSchema(internalSchema);
		if (schemaDescription.trim()) {
			standardSchema.description = schemaDescription.trim();
		}
		onSave(standardSchema);
		onClose();
	};

	const handleReset = () => {
		setInternalSchema({
			type: "object",
			properties: {},
		});
	};

	const handleAddProperty = () => {
		const existingKeys = Object.keys(internalSchema.properties || {});
		let counter = 1;
		let newName = `property-${counter}`;
		while (existingKeys.includes(newName)) {
			counter++;
			newName = `property-${counter}`;
		}

		const newProperty: JsonSchemaEditorInternalSchemaProperty = {
			id: nanoid(),
			type: "string",
			isArray: false,
			isRequired: false,
		};

		handleInternalChange({
			...internalSchema,
			properties: {
				...internalSchema.properties,
				[newName]: newProperty,
			},
		});
	};

	const handleUpdateProperty = (
		name: string,
		property: JsonSchemaEditorInternalSchemaProperty,
	) => {
		handleInternalChange({
			...internalSchema,
			properties: {
				...internalSchema.properties,
				[name]: property,
			},
		});
	};

	const handleRenameProperty = (oldName: string, newName: string) => {
		if (oldName === newName) {
			return;
		}

		const newProperties: Record<
			string,
			JsonSchemaEditorInternalSchemaProperty
		> = {};
		for (const [key, value] of Object.entries(internalSchema.properties)) {
			if (key === oldName) {
				newProperties[newName] = value;
			} else {
				newProperties[key] = value;
			}
		}

		handleInternalChange({
			...internalSchema,
			properties: newProperties,
		});
	};

	const handleDeleteProperty = (name: string) => {
		const newProperties = { ...internalSchema.properties };
		delete newProperties[name];

		handleInternalChange({
			...internalSchema,
			properties: newProperties,
		});
	};

	return (
		<div className={cn("flex flex-col h-full gap-4", className)}>
			<DialogHeader className="flex flex-row justify-start gap-4">
				<div className="flex flex-col gap-2">
					<DialogTitle>Schema Editor</DialogTitle>
					<DialogDescription>
						Edit the schema for the agent output
					</DialogDescription>
				</div>
				<div className="flex items-center gap-2">
					<Button onClick={handleReset} variant="outline" size="sm">
						Reset
					</Button>
					<Button onClick={handleSave} size="sm">
						<Save size={16} className="mr-1" />
						Save
					</Button>
				</div>
			</DialogHeader>
			<div className="space-y-4">
				<label
					htmlFor="schema-description"
					className="text-sm font-medium"
				>
					Schema Description
				</label>
				<Input
					id="schema-description"
					value={schemaDescription}
					onChange={(e) => setSchemaDescription(e.target.value)}
					placeholder="Overall schema description (optional)"
					className="w-full"
				/>
			</div>
			<div className="flex-1 flex flex-col overflow-y-auto">
				{Object.entries(internalSchema.properties || {}).map(
					([name, property]) => (
						<JsonSchemaEditorPropertyRow
							key={property.id}
							name={name}
							property={property}
							onUpdate={(updated) =>
								handleUpdateProperty(name, updated)
							}
							onRename={(newName) =>
								handleRenameProperty(name, newName)
							}
							onDelete={() => handleDeleteProperty(name)}
							onAddProperty={handleAddProperty}
							level={0}
						/>
					),
				)}

				<Button
					onClick={handleAddProperty}
					variant="ghost"
					size="sm"
					className="w-full justify-start mt-2"
				>
					<Plus size={16} />
					Add Property
				</Button>
			</div>
		</div>
	);
}

interface JsonSchemaEditorPropertyRowProps {
	name: string;
	property: JsonSchemaEditorInternalSchemaProperty;
	onUpdate: (property: JsonSchemaEditorInternalSchemaProperty) => void;
	onRename: (newName: string) => void;
	onDelete: () => void;
	onAddProperty: (parentPath?: string) => void;
	level: number;
}

function JsonSchemaEditorPropertyRow({
	name,
	property,
	onUpdate,
	onRename,
	onDelete,
	onAddProperty,
	level,
}: JsonSchemaEditorPropertyRowProps) {
	const isNested = property.type === "object";
	const canNest = level < MAX_NESTING_LEVEL;

	const handleAddNestedProperty = () => {
		if (!isNested) {
			return;
		}

		const existingKeys = Object.keys(property.properties || {});
		let counter = 1;
		let newName = `property-${counter}`;
		while (existingKeys.includes(newName)) {
			counter++;
			newName = `property-${counter}`;
		}

		const newProperty: JsonSchemaEditorInternalSchemaProperty = {
			id: nanoid(),
			type: "string",
			isArray: false,
			isRequired: false,
		};

		onUpdate({
			...property,
			properties: {
				...(property.properties || {}),
				[newName]: newProperty,
			},
		});
	};

	const handleDeleteNestedProperty = (propName: string) => {
		if (!isNested || !property.properties) {
			return;
		}

		const newProperties = { ...property.properties };
		delete newProperties[propName];

		onUpdate({
			...property,
			properties: newProperties,
		});
	};

	const handleUpdateNestedProperty = (
		propName: string,
		updatedProperty: JsonSchemaEditorInternalSchemaProperty,
	) => {
		if (!isNested) {
			return;
		}

		onUpdate({
			...property,
			properties: {
				...(property.properties || {}),
				[propName]: updatedProperty,
			},
		});
	};

	const handleRenameNestedProperty = (oldName: string, newName: string) => {
		if (!isNested || !property.properties || oldName === newName) {
			return;
		}

		const newProperties: Record<
			string,
			JsonSchemaEditorInternalSchemaProperty
		> = {};
		for (const [key, value] of Object.entries(property.properties)) {
			if (key === oldName) {
				newProperties[newName] = value;
			} else {
				newProperties[key] = value;
			}
		}

		onUpdate({
			...property,
			properties: newProperties,
		});
	};

	return (
		<div className="relative">
			{level > 0 && (
				<div
					className="absolute top-0 bottom-0 w-0.5 bg-border"
					style={{ left: `${(level - 1) * 20 + 20}px` }}
				/>
			)}

			<div
				className="flex items-center gap-2 py-2 px-3 hover:bg-muted/50 rounded-sm transition-colors w-full"
				style={{ paddingLeft: `${level * 20 + 12}px` }}
			>
				<Input
					value={name}
					onChange={(e) => {
						onRename(e.target.value);
					}}
					className="flex-1 min-w-0 h-8"
					placeholder="property-name"
				/>

				<Input
					value={property.description || ""}
					onChange={(e) => {
						onUpdate({
							...property,
							description: e.target.value,
						});
					}}
					className="flex-1 min-w-0 h-8 text-xs"
					placeholder="Description (optional)"
				/>

				<Select
					value={property.type}
					onValueChange={(newType) => {
						const updated: JsonSchemaEditorInternalSchemaProperty =
							{
								...property,
								type: newType as JsonSchemaEditorJSONSchemaType,
							};

						if (newType === "object" && !property.properties) {
							updated.properties = {};
						}

						if (newType !== "enum") {
							updated.enumValues = undefined;
						}

						onUpdate(updated);
					}}
				>
					<SelectTrigger size="sm" className="w-28 shrink-0">
						<SelectValue placeholder="Type" />
					</SelectTrigger>
					<SelectContent>
						{SCHEMA_TYPES.map((type) => (
							<SelectItem key={type} value={type}>
								{type}
							</SelectItem>
						))}
					</SelectContent>
				</Select>

				<Button
					variant={property.isArray ? "default" : "outline"}
					size="icon-sm"
					onClick={() =>
						onUpdate({
							...property,
							isArray: !property.isArray,
						})
					}
				>
					<Brackets size={16} />
				</Button>

				<Button
					variant={property.isRequired ? "default" : "outline"}
					size="icon-sm"
					onClick={() =>
						onUpdate({
							...property,
							isRequired: !property.isRequired,
						})
					}
				>
					<Asterisk size={16} />
				</Button>

				<Button variant="destructive" size="sm" onClick={onDelete}>
					<Trash2 size={16} />
				</Button>
			</div>

			{property.type === "enum" && (
				<div
					className="py-2"
					style={{ paddingLeft: `${level * 20 + 12}px` }}
				>
					<div className="text-xs font-medium mb-2 text-muted-foreground">
						Enum Values:
					</div>
					<div className="space-y-1 mb-2 ml-4">
						{(property.enumValues || []).map((value, index) => (
							<div
								key={`enum-${value}`}
								className="flex items-center gap-2"
							>
								<Input
									value={value}
									onChange={(e) => {
										const newEnumValues = [
											...(property.enumValues || []),
										];
										newEnumValues[index] = e.target.value;
										onUpdate({
											...property,
											enumValues: newEnumValues,
										});
									}}
									className="flex-1 h-8"
									placeholder="Enum value"
								/>
								<Button
									variant="destructive"
									size="sm"
									onClick={() => {
										onUpdate({
											...property,
											enumValues: (
												property.enumValues || []
											).filter((_, i) => i !== index),
										});
									}}
								>
									<Trash2 size={16} />
								</Button>
							</div>
						))}
					</div>
					<div className="flex gap-2 ml-4">
						<Button
							variant="outline"
							size="sm"
							onClick={() => {
								onUpdate({
									...property,
									enumValues: [
										...(property.enumValues || []),
										"",
									],
								});
							}}
							className="h-7 px-2 text-xs"
						>
							<Plus size={12} className="mr-1" />
							Add Value
						</Button>
					</div>
				</div>
			)}

			{isNested && canNest && (
				<div>
					{Object.entries(property.properties || {}).map(
						([propName, prop]) => (
							<JsonSchemaEditorPropertyRow
								key={prop.id}
								name={propName}
								property={prop}
								onUpdate={(updated) =>
									handleUpdateNestedProperty(
										propName,
										updated,
									)
								}
								onRename={(newName) =>
									handleRenameNestedProperty(
										propName,
										newName,
									)
								}
								onDelete={() =>
									handleDeleteNestedProperty(propName)
								}
								onAddProperty={onAddProperty}
								level={level + 1}
							/>
						),
					)}

					<Button
						onClick={handleAddNestedProperty}
						variant="ghost"
						size="sm"
						className="w-full justify-start"
						style={{ paddingLeft: `${(level + 1) * 20 + 16}px` }}
					>
						<Plus size={16} />
						Add Property
					</Button>
				</div>
			)}
		</div>
	);
}

interface JsonSchemaEditorDialogProps {
	schema: JSONSchema7 | null;
	onSave: (schema: JSONSchema7) => void;
}

export function JsonSchemaEditorDialog({
	schema,
	onSave,
}: JsonSchemaEditorDialogProps) {
	const [dialogOpen, setDialogOpen] = useState(false);

	return (
		<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
			<DialogTrigger asChild>
				<Button variant="outline" size="sm" className="w-full">
					{schema ? "Edit Schema" : "Create Schema"}
				</Button>
			</DialogTrigger>
			<DialogContent className="sm:max-w-4xl h-full sm:max-h-[90vh] flex flex-col overflow-hidden">
				<JsonSchemaEditor
					initialSchema={schema || { type: "object", properties: {} }}
					onSave={onSave}
					onClose={() => setDialogOpen(false)}
				/>
			</DialogContent>
		</Dialog>
	);
}

// ============================================================================
// Schema Preview
// ============================================================================

function parseJSONSchema(
	schema: JSONSchema7,
): Record<string, ReturnType<typeof parseJSONSchemaProperty>> {
	if (!schema || typeof schema !== "object" || !schema.properties) {
		return {};
	}

	const properties: Record<
		string,
		ReturnType<typeof parseJSONSchemaProperty>
	> = {};
	for (const [name, prop] of Object.entries(
		schema.properties as Record<string, unknown>,
	)) {
		if (typeof prop === "object" && prop !== null) {
			properties[name] = parseJSONSchemaProperty(
				prop as Record<string, unknown>,
			);
		}
	}

	return properties;
}

function parseSchemaToPreview(
	schema: JSONSchema7,
): JsonSchemaPreviewProperty[] {
	const parsedProperties = parseJSONSchema(schema);
	const required = Array.isArray(schema.required) ? schema.required : [];

	const convertToSchemaProperty = (
		name: string,
		parsed: ReturnType<typeof parseJSONSchemaProperty>,
		isRequired: boolean,
	): JsonSchemaPreviewProperty => {
		const result: JsonSchemaPreviewProperty = {
			name,
			type: parsed.type,
			isArray: parsed.isArray,
			isRequired,
			description: parsed.description,
			properties: undefined,
			enumValues: parsed.enumValues,
		};

		if (parsed.properties) {
			result.properties = [];
			for (const [nestedName, nestedParsed] of Object.entries(
				parsed.properties,
			)) {
				result.properties.push(
					convertToSchemaProperty(nestedName, nestedParsed, false),
				);
			}
		}

		return result;
	};

	const properties: JsonSchemaPreviewProperty[] = [];
	for (const [name, parsed] of Object.entries(parsedProperties)) {
		properties.push(
			convertToSchemaProperty(name, parsed, required.includes(name)),
		);
	}

	return properties;
}

interface JsonSchemaPreviewProps extends ComponentProps<"div"> {
	schema: JSONSchema7;
}

export function JsonSchemaPreview({
	schema,
	className,
	...props
}: JsonSchemaPreviewProps) {
	const properties = parseSchemaToPreview(schema);

	return (
		<div
			className={cn("border rounded-md overflow-hidden", className)}
			{...props}
		>
			<div className="p-2 border-b bg-muted/50">
				<h4 className="text-xs font-semibold">Schema Structure</h4>
			</div>
			<div className="max-h-[200px] overflow-y-auto p-1">
				{properties.length === 0 ? (
					<div className="p-4 text-xs text-muted-foreground text-center">
						No properties defined
					</div>
				) : (
					properties.map((property) => (
						<JsonSchemaPreviewPropertyRow
							key={property.name}
							property={property}
						/>
					))
				)}
			</div>
		</div>
	);
}

interface JsonSchemaPreviewProperty {
	name: string;
	type: string;
	isArray: boolean;
	isRequired: boolean;
	description?: string;
	properties?: JsonSchemaPreviewProperty[];
	enumValues?: string[];
}

function JsonSchemaPreviewPropertyRow({
	property,
	level = 0,
}: {
	property: JsonSchemaPreviewProperty;
	level?: number;
}) {
	const typeDisplay = property.isArray ? `${property.type}[]` : property.type;

	return (
		<div>
			<div
				className="flex items-start gap-2 px-3 py-2 text-xs rounded-sm"
				style={{ paddingLeft: `${8 + level * 16}px` }}
			>
				<div className="flex-1 text-left min-w-0">
					<div className="flex items-center gap-2">
						<span className="font-mono font-semibold">
							{property.name}
						</span>
						{property.isRequired && (
							<span className="text-red-500 text-[10px]">*</span>
						)}
					</div>
					{property.description && (
						<div className="text-muted-foreground mt-0.5 text-[11px]">
							{property.description}
						</div>
					)}
					{property.type === "enum" && property.enumValues && (
						<div className="text-muted-foreground mt-1 text-[10px]">
							Values: {property.enumValues.join(", ")}
						</div>
					)}
				</div>
				<span
					className={cn(
						"px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0",
						"bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300",
					)}
				>
					{typeDisplay}
				</span>
			</div>
			{property.properties && property.properties.length > 0 && (
				<div>
					{property.properties.map((nestedProp) => (
						<JsonSchemaPreviewPropertyRow
							key={nestedProp.name}
							property={nestedProp}
							level={level + 1}
						/>
					))}
				</div>
			)}
		</div>
	);
}

export function JsonSchemaPreviewEmpty({
	className,
	...props
}: ComponentProps<"div">) {
	return (
		<div
			className={cn(
				"border rounded-md p-4 text-xs text-muted-foreground text-center",
				className,
			)}
			{...props}
		>
			No schema defined
		</div>
	);
}

Installation

npx shadcn@latest add @simple-ai/json-schema-editor

Usage

import { JsonSchemaEditor } from "@/components/ui/json-schema-editor"
<JsonSchemaEditor />