@zod-monaco/monaco
v4.0.1
Published
Monaco editor adapter with Zod-powered JSON validation, hover, and completions
Maintainers
Readme
@zod-monaco/monaco
Monaco editor adapter with Zod-powered JSON validation, hover tooltips, completions, suggestion refinements, and AI-safe editing.
Installation
npm install @zod-monaco/monaco @zod-monaco/core zodMonaco Loading
This package loads Monaco editor from CDN (v0.52.2) via @monaco-editor/loader. Use loadMonaco() to load it:
import { loadMonaco } from "@zod-monaco/monaco";
const monaco = await loadMonaco();Usage
import { loadMonaco, createZodEditorController } from "@zod-monaco/monaco";
import { describeSchema } from "@zod-monaco/core";
import { z } from "zod";
const monaco = await loadMonaco();
const descriptor = describeSchema(
z.object({ name: z.string(), age: z.number() }),
);
const controller = createZodEditorController({
monaco,
descriptor,
value: '{ "name": "", "age": 0 }',
});
const editor = controller.mount(document.getElementById("editor")!);
controller.onChange((value) => {
console.log("JSON changed:", value);
});
controller.onValidationChange((result) => {
console.log("Valid:", result.valid, "Issues:", result.issues);
});Features
All features are enabled by default and can be toggled:
const controller = createZodEditorController({
monaco,
descriptor,
features: {
hover: true, // metadata hover tooltips
validation: true, // JSON Schema structural validation
diagnostics: true, // Zod runtime validation markers
completions: true, // enum value completions + suggestion refinements
},
});Suggestion Refinements
Inject runtime suggestions into free-text fields without touching the schema:
const controller = createZodEditorController({
monaco,
descriptor,
refinements: [
{
path: ["template"],
suggestions: ["{Name}", "{Price}", "{Category}"],
triggerPattern: "\\{",
},
],
});triggerPattern is a regex string. When provided, suggestions only appear after the text before the cursor matches that pattern. Simple single-character patterns ("@", "\\{") are also registered as Monaco trigger characters — completions open automatically when the user types that character.
Update refinements at runtime without remounting:
controller.setRefinements([
{
path: ["template"],
suggestions: ["{Name}", "{Price}"],
triggerPattern: "\\{",
},
]);Completion priority: Enum values from the JSON Schema always take precedence. Suggestion refinements are shown only when no enum values exist for the current field.
Suggestion refinements are soft — they do not add validation constraints. For strict enum validation, use EnumRefinement in describeSchema.
Attach to Existing Editor
If you already have a Monaco editor instance, use attachZodToEditor to add Zod features without replacing your setup:
import { attachZodToEditor } from "@zod-monaco/monaco";
const attachment = attachZodToEditor({
monaco,
editor,
descriptor,
refinements: [
{ path: ["content"], suggestions: ["{Name}", "{Price}"], triggerPattern: "\\{" },
],
});
attachment.setDescriptor(anotherDescriptor); // swap schema at runtime
attachment.setRefinements([...]); // update suggestions at runtime
attachment.onValidationChange((result) => console.log(result));
attachment.dispose(); // removes Zod features, does NOT dispose the editorAI-Safe Editing — prepareJsonEdit
prepareJsonEdit applies an AI-generated value safely: it validates and diffs without touching the editor. The user reviews the diff before anything is written.
import { prepareJsonEdit } from "@zod-monaco/monaco";
import { buildFieldCatalog } from "@zod-monaco/core";
// 1. Build catalog and send to AI (app's responsibility)
const catalog = buildFieldCatalog(descriptor, { currentValue });
const aiResponse = await callYourAI(catalog); // { value: {...} }
// 2. Prepare — editor is NOT modified
const prepared = prepareJsonEdit(editor, descriptor, aiResponse.value);
// 3. Inspect before showing review UI
prepared.valid; // boolean — passes Zod validation?
prepared.validationIssues; // ValidationIssue[] — { path, pointer, message }
prepared.diff; // FieldDiff[] — added / removed / changed
prepared.newText; // formatted JSON string (use in diff editor)
prepared.stale; // true if editor was edited during review
// 4. On accept:
if (prepared.stale) {
// editor changed while reviewing — call prepareJsonEdit again
} else {
prepared.commit(); // now writes to editor via executeEdits
}
// 5. On reject — nothing to undo, editor was never touchedcommit() throws if valid is false. Use { force: true } to override:
prepared.commit({ force: true });Localization
import { createZodEditorController, locales } from "@zod-monaco/monaco";
createZodEditorController({ monaco, descriptor, locale: locales.tr }); // Turkish
createZodEditorController({
monaco,
descriptor,
locale: { ...locales.en, required: "Pflichtfeld" },
});Available built-ins: locales.en (default), locales.tr.
Core fields: required, optional, examples, placeholder, enumValues, defaultValue, readOnly. Optional fields: schemaBranch, constraints (used by worker-enhanced hover).
Multiple Editors
Multiple editors sharing the same Monaco instance are supported. Each editor's schema is managed through an internal registry — disposing one does not affect others — no extra setup required.
API
createZodEditorController(options)
mount(element)— mount editor to a DOM elementgetValue()/setValue(value)— read/write editor contentsetDescriptor(descriptor)— update schema without remountingsetRefinements(refinements)— update suggestion refinements at runtimeonChange(listener)— subscribe to content changesonValidationChange(listener)— subscribe to validation resultsonCursorPathChange(listener)— subscribe to breadcrumb path changesrevealIssue(issue)— navigate to a Zod issue in the editorrevealPath(path)— navigate to a path in the editorformat()— format JSON (also bound to Ctrl+S)updateOptions(options)— update editor options at runtimegetMonaco()/getRawEditor()— escape hatch to native Monaco APIsdispose()— cleanup
Options also accept:
diagnosticsOptions— base Monaco JSON diagnostics merged under registry-managed fieldsdisableWorker— disable worker-based schema enhancements (falls back to sync parser)onReadOnlyViolation(detail)— callback with{ path: FieldPath, operation: "type" | "paste" | "delete" | "replace" }
attachZodToEditor(options)
Returns a ZodEditorAttachment:
setDescriptor(descriptor)— swap schema at runtimesetRefinements(refinements)— update suggestion refinements at runtimeonValidationChange(listener)— subscribe to validation resultsonCursorPathChange(listener)— subscribe to breadcrumb path changesdispose()— remove Zod features (does NOT dispose the editor)
buildBreadcrumbLabelCache(descriptor)
Pre-computes breadcrumb labels from the schema catalog for O(1) lookup. Pass to buildBreadcrumbSegments for performance-optimized cursor tracking.
import { buildBreadcrumbLabelCache, buildBreadcrumbSegments } from "@zod-monaco/monaco";
const cache = buildBreadcrumbLabelCache(descriptor);
const segments = buildBreadcrumbSegments(path, descriptor, schemaCache, cache);
// segments[1].title — "Owner" (from metadata)
// segments[1].readOnly — true/falseprepareJsonEdit(editor, descriptor, newValue)
Returns a PreparedEdit:
| Field | Type | Description |
| ----- | ---- | ----------- |
| newText | string | Formatted JSON of the proposed value |
| valid | boolean | Passes Zod validation |
| validationIssues | ValidationIssue[] | { path, pointer, message } per issue |
| diff | FieldDiff[] | Added / removed / changed fields |
| stale | boolean (getter) | Editor changed since prepare was called |
| commit(opts?) | void | Write to editor — throws if invalid or stale |
License
MIT
