SchemaView
Show a JSON Schema, such as an MCP tool inputSchema, as a parameter reference. From 560px of width it is a table with name, type, description and default; narrower it becomes stacked rows. Required parameters get a badge, enums and const unions list allowed values, ranges and formats are shown, and nested objects, arrays of objects and oneOf/anyOf variants collapse (defaultExpandedDepth). flattenSchema returns the rows for custom renderers.
Getting Started
$pnpm add @sinups/ai-kit @mantine/core @mantine/hooks @tabler/icons-reactExamples
Wide
Narrow
Source
The component source as it ships in the package. Useful as a reference, or as a starting point if you want to fork a piece.
primitives/SchemaView/SchemaView.tsx
import React, { Fragment, memo, useMemo, useState } from 'react';
import {
ActionIcon,
Badge,
Box,
Code,
Divider,
EmptyState,
Group,
Stack,
Table,
Text,
} from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import { IconBraces, IconChevronRight } from '@tabler/icons-react';
import { cx } from '../../utils/cx';
import {
flattenSchema,
getDefaultExpandedPaths,
getVisibleSchemaRows,
type JsonSchema,
type SchemaRow,
} from './schema';
import classes from './SchemaView.module.css';
export type SchemaViewLabels = {
/** Name column header, `Name` by default */
name: string;
/** Type column header, `Type` by default */
type: string;
/** Description column header, `Description` by default */
description: string;
/** Default column header and narrow prefix, `Default` by default */
default: string;
/** Badge on required parameters, `required` by default */
required: string;
/** Prefix of allowed values, `One of` by default */
oneOf: string;
/** Accessible label prefix of the expand button, `Expand` by default */
expand: string;
/** Accessible label prefix of the collapse button, `Collapse` by default */
collapse: string;
/** Shown when the schema has no properties, `No parameters` by default */
empty: string;
};
export interface SchemaViewProps {
/** JSON Schema of an object, for example a tool `inputSchema` */
schema: JsonSchema;
/** Nesting levels expanded initially, `1` by default (top-level objects open, deeper ones closed) */
defaultExpandedDepth?: number;
/** Component width in px from which parameters are shown as a table, `560` by default */
breakpoint?: number;
/** Overrides of the default English labels */
labels?: Partial<SchemaViewLabels>;
/** Class name added to the root element */
className?: string;
/** Inline styles added to the root element */
style?: React.CSSProperties;
}
export const DEFAULT_SCHEMA_VIEW_LABELS: SchemaViewLabels = {
name: 'Name',
type: 'Type',
description: 'Description',
default: 'Default',
required: 'required',
oneOf: 'One of',
expand: 'Expand',
collapse: 'Collapse',
empty: 'No parameters',
};
function formatRange(row: SchemaRow): string | null {
if (row.minimum !== undefined && row.maximum !== undefined) {
return `${row.minimum} – ${row.maximum}`;
}
if (row.minimum !== undefined) {
return `≥ ${row.minimum}`;
}
if (row.maximum !== undefined) {
return `≤ ${row.maximum}`;
}
return null;
}
type RowPartProps = {
row: SchemaRow;
labels: SchemaViewLabels;
};
function NameCell({
row,
labels,
expanded,
withToggleSpace,
onToggle,
}: RowPartProps & {
expanded: boolean;
withToggleSpace: boolean;
onToggle: () => void;
}) {
const toggleLabel = `${expanded ? labels.collapse : labels.expand} ${row.name}`;
return (
<Group
gap={4}
wrap="nowrap"
className={classes.name}
style={{ '--schema-depth': row.depth } as React.CSSProperties}
>
{row.hasChildren ? (
<ActionIcon
size="sm"
variant="subtle"
color="gray"
aria-label={toggleLabel}
aria-expanded={expanded}
onClick={onToggle}
>
<IconChevronRight
size={14}
className={classes.chevron}
data-expanded={expanded || undefined}
/>
</ActionIcon>
) : (
withToggleSpace && <Box className={classes.toggleSpace} />
)}
<Text size="sm" ff="monospace" fw={500} className={classes.breakAll}>
{row.name}
</Text>
{row.required && (
<Badge size="xs" variant="light" color="red" className={classes.badge}>
{labels.required}
</Badge>
)}
</Group>
);
}
function TypeCell({ row }: { row: SchemaRow }) {
return (
<Group gap={4} wrap="wrap">
<Code>{row.type}</Code>
{row.format && (
<Text size="xs" c="dimmed">
{row.format}
</Text>
)}
</Group>
);
}
function DescriptionCell({ row, labels }: RowPartProps) {
const range = formatRange(row);
return (
<Stack gap={4}>
{row.description && <Text size="sm">{row.description}</Text>}
{row.enum && (
<Group gap={4} wrap="wrap">
<Text size="xs" c="dimmed">
{labels.oneOf}
</Text>
{row.enum.map((value, index) => (
<Code key={index}>{JSON.stringify(value)}</Code>
))}
</Group>
)}
{range && (
<Text size="xs" c="dimmed">
{range}
</Text>
)}
</Stack>
);
}
/** JSON Schema as a parameter reference: a table when wide, stacked rows when narrow, nested objects collapsible */
export const SchemaView = memo(function SchemaView({
schema,
defaultExpandedDepth = 1,
breakpoint = 560,
labels: labelsOverride,
className,
style,
}: SchemaViewProps) {
const labels = { ...DEFAULT_SCHEMA_VIEW_LABELS, ...labelsOverride };
const { ref, width } = useElementSize();
const isWide = width >= breakpoint;
const rows = useMemo(() => flattenSchema(schema), [schema]);
const [state, setState] = useState(() => ({
rows,
expanded: getDefaultExpandedPaths(rows, defaultExpandedDepth),
}));
if (state.rows !== rows) {
setState({ rows, expanded: getDefaultExpandedPaths(rows, defaultExpandedDepth) });
}
const visible = getVisibleSchemaRows(rows, state.expanded);
const withToggleSpace = rows.some((row) => row.hasChildren);
const toggle = (path: string) => {
setState((prev) => {
const expanded = new Set(prev.expanded);
if (expanded.has(path)) {
expanded.delete(path);
} else {
expanded.add(path);
}
return { ...prev, expanded };
});
};
const nameCell = (row: SchemaRow) => (
<NameCell
row={row}
labels={labels}
expanded={state.expanded.has(row.path)}
withToggleSpace={withToggleSpace}
onToggle={() => toggle(row.path)}
/>
);
let content: React.ReactNode;
if (rows.length === 0) {
content = <EmptyState size="sm" icon={<IconBraces />} title={labels.empty} />;
} else if (isWide) {
content = (
<Table verticalSpacing="xs" className={classes.table}>
<Table.Thead>
<Table.Tr>
<Table.Th>{labels.name}</Table.Th>
<Table.Th>{labels.type}</Table.Th>
<Table.Th>{labels.description}</Table.Th>
<Table.Th>{labels.default}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{visible.map((row) => (
<Table.Tr key={row.path}>
<Table.Td>{nameCell(row)}</Table.Td>
<Table.Td>
<TypeCell row={row} />
</Table.Td>
<Table.Td>
<DescriptionCell row={row} labels={labels} />
</Table.Td>
<Table.Td>
{row.default !== undefined && <Code>{JSON.stringify(row.default)}</Code>}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
);
} else {
content = (
<Stack gap={0}>
{visible.map((row, index) => (
<Fragment key={row.path}>
{index > 0 && <Divider />}
<Stack gap={4} py="xs" data-path={row.path}>
<Group justify="space-between" gap="xs" wrap="nowrap" align="flex-start">
{nameCell(row)}
<TypeCell row={row} />
</Group>
<Box
className={classes.detail}
data-with-toggle={withToggleSpace || undefined}
style={{ '--schema-depth': row.depth } as React.CSSProperties}
>
<DescriptionCell row={row} labels={labels} />
{row.default !== undefined && (
<Group gap={4} mt={4}>
<Text size="xs" c="dimmed">
{labels.default}
</Text>
<Code>{JSON.stringify(row.default)}</Code>
</Group>
)}
</Box>
</Stack>
</Fragment>
))}
</Stack>
);
}
return (
<Box
ref={ref}
className={cx(classes.root, className)}
style={style}
data-measuring={width === 0 || undefined}
>
{content}
</Box>
);
});
SchemaView.displayName = 'SchemaView';
API reference
Prop
Type
Required
schema
JsonSchema
Yes
JSON Schema of an object, for example a tool `inputSchema`
defaultExpandedDepth
number
No
Nesting levels expanded initially, `1` by default (top-level objects open, deeper ones closed)
breakpoint
number
No
Component width in px from which parameters are shown as a table, `560` by default
labels
Partial<SchemaViewLabels>
No
Overrides of the default English labels
className
string
No
Class name added to the root element
style
React.CSSProperties
No
Inline styles added to the root element