# vue-json-ui-editor: a JSON Schema form editor that lets you swap the widget library

> vue-json-ui-editor renders a form from a JSON Schema, and the design decision that makes it more than a schema renderer is that it ships with no UI library at all. The element-plus components are registered by the application through a static API, per type, and a per-instance override lets two editors on the same page use different widgets. The result is a schema form editor that does not decide what your controls look like.

**yourtion/vue-json-ui-editor** — Edit JSON in UI form with JSON Schema and Vue.js

- Repository: https://github.com/yourtion/vue-json-ui-editor
- Website: http://jsonui.js.org/
- Stars: 526 · Forks: 91
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/yourtion-vue-json-ui-editor

## What it is, and the idea that shapes it

vue-json-ui-editor is a Vue 3 JSON Schema based form editor component, for editing JSON in UI form with JSON Schema and element-plus, with full TypeScript support.

What the repository description does not say, and what the README makes clear further down, is that element-plus is not a dependency. The editor renders with native HTML elements by default. To style it with element-plus you register the components yourself through a static API, and the README points at the example file that does it.

That is the whole design in one decision. A schema-driven form editor has to answer the question of what a text field looks like, and the usual answer is to pick a UI library and depend on it. This one answers that the question belongs to the application, and provides a registry so the application can answer it.

The consequences are worth spelling out. The package does not drag element-plus into your bundle if you do not want it, and it does not prevent you from using it. You can register a Vue component or a native tag name for each field type, and the editor renders whatever you registered. If you want a bare, dependency-free form for an admin tool, you leave the registry empty and get plain inputs. If you want the element-plus look, you register six components and get it.

The primary language is TypeScript, the licence is MIT, the default branch is main, and the homepage is jsonui.js.org. The last push was on 2026-07-02 with the project not archived, and the repository publishes no GitHub releases. The package version in the manifest is 3.1.0.

The versions and branches table is a separate piece of information. The main branch is 3.x on Vue 3 with element-plus and is the active one. The master branch is 2.x on Vue 2 with element-ui, in maintenance. And the v1 branch is 1.x, marked legacy. So the project maintains three major lines in parallel, one per Vue generation, and the branch you pick determines the Vue version and the element library it expects.

## Installing it and mounting a first editor

Installation is one command, with the package manager variant alongside:

```bash
npm install vue-json-ui-editor
# or
pnpm add vue-json-ui-editor
```

The version note repeats the branch table in prose. The 3.x line targets Vue 3 and is designed to work with element-plus, and for Vue 2 you need the master or v1 branch instead.

Then the component usage, and it is a complete example rather than a fragment:

```vue
<template>
  <json-editor ref="jsonEditorRef" :schema="schema" v-model="model">
    <el-button type="primary" @click="submit">Submit</el-button>
    <el-button @click="reset">Reset</el-button>
  </json-editor>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import JsonEditor from 'vue-json-ui-editor';

const schema = {
  type: 'object',
  title: 'vue-json-editor demo',
  properties: {
    name: { type: 'string' },
    email: { type: 'string' },
  },
};

const model = ref({ name: 'Yourtion' });
const jsonEditorRef = ref<InstanceType<typeof JsonEditor>>();

function submit() {
  // jsonEditorRef.value?.form() returns the underlying element-plus form instance
  jsonEditorRef.value?.form().validate((valid: boolean) => {
    if (!valid) {
      jsonEditorRef.value?.setErrorMessage('Please fill out the required fields');
    }
  });
}

function reset() {
  jsonEditorRef.value?.reset();
}
</script>
```

The template puts the editor inside a json-editor tag with a schema prop and a v-model, and the two buttons are slotted children, so the editor provides a default slot for your own action buttons. That is a good detail, because it means the editor does not impose a submit button layout on you.

The script uses the composition API with script setup and lang ts, and the schema is an ordinary object literal with a type of object, a title, and a properties map.

The interesting part is the ref usage at the bottom. The ref is typed as InstanceType of typeof JsonEditor, and the comment explains what that gives you: the form method returns the underlying element-plus form instance, and validate is called on it with a callback that receives a validity boolean, and if it is not valid the code calls setErrorMessage on the editor. The reset function calls the editor's own reset method.

So the public surface is a component with a typed instance handle, and the handle exposes both convenience methods and the underlying form component. The schema prop is required and the documentation notes that you should use the v-if directive to load an asynchronous schema, which is a practical hint rather than an obvious one, because a schema fetched at runtime will otherwise be undefined on first render.

## The props, and the per-instance component override

The props are a short list, and one of them is where the interesting design is.

The schema prop is the JSON Schema object and it is required. The v-model or modelValue prop is the two-way binding for the form data, optional with a default of an empty object, and the note explains that in Vue 3 v-model binds to the modelValue prop.

Then there is a set of smaller ones. auto-complete is a string controlling whether the browser can complete the value of the control, with the possible values off and on. no-validate is a boolean indicating the form is not to be validated on submit. input-wrapping-class wraps each field's controls in a div with a class, and leaving it undefined disables the wrapping.

And then components, which is the one that changes the library's character. Per-instance component overrides, and when provided they are merged over the global defaults registered via JsonEditor.setComponent, so multiple json-editor instances on the same page can each use a different UI library without polluting each other. The keys are element types such as text, select, form and label, and the values may be a full component and option config or a shorthand component name or object. Leaving it undefined falls back to the global registry.

The example is two editors on the same page with different text widgets:

```js
<json-editor :schema="a" :components="{ text: CompA }" />
<json-editor :schema="b" :components="{ text: CompB }" />
```

That is a genuinely useful capability, and it is unusual. A schema form editor that hard-codes a widget library cannot do this, because both editors share the registry. Here the per-instance map is merged over the global one, so a page can have an admin form in one design system and a public form in another without either bleeding into the other.

The reason it matters in practice is that most applications end up with more than one form style. A public-facing form might want plain inputs for a fast page, while an internal tool wants a full component set with validation styling. With a single global registry you would have to pick one and live with it in both places.

## The static registration API, and the element-plus label trap

The static API is the global counterpart to the per-instance override, and it is where the element-plus integration lives.

JsonEditor.setComponent takes a type, a component and an optional option. You register a Vue component, or a native tag name, for a given field or element type, with the examples given for form, label, email, text, select and error among others. The option may be a plain object or a factory callback receiving the view-model, the field and the item.

The three examples show the pattern:

```js
JsonEditor.setComponent('text', 'el-input');
JsonEditor.setComponent('form', 'el-form', ({ vm }) => ({ model: vm.model, rules: {} }));
JsonEditor.setComponent('error', 'el-alert', ({ vm }) => ({ type: 'error', title: vm.error }));
```

So text maps to el-input, form maps to el-form with a callback that returns the model and an empty rules object, and error maps to el-alert with a callback that returns the type of error and the title from the view-model's error value. The callback form is what makes this work, because element-plus components need props that depend on the current state, and a static registration cannot know that.

The note about label with element-plus is the useful warning in this section. el-form-item reads its label text from the label prop and not from the default slot, so the label registration callback must return label: field.label in addition to prop: field.name. And the reason it is spelled out is that this is the kind of detail that would otherwise be a silent rendering bug, a form item with no visible label, in a library whose entire purpose is generating forms from a schema.

The README then covers the alternative approach of choosing the widget for a field through attrs in the schema property, which is the per-field override rather than the per-type one, and there is a section heading for it. The schema is read as the field descriptor through schema.attrs, so a field can carry its own type. That is a third level of specificity: a global default per type, an instance override per type, and a per-field choice.

## The methods, events and view-model escape hatches

The methods list is the practical part of the API, and it is exposed through a template ref on the editor instance.

input takes a name and gets a form input reference. form gets the rendered form component instance, for example the element-plus el-form, so you can call validate or resetFields on it. checkValidity checks whether the form satisfies its constraints and returns a boolean. validate is an async validation shortcut that delegates to the underlying form component's validate when one is available. reset resets the value of all elements of the form to the initial modelValue. setErrorMessage sets an error message rendered through the error component type, and clearErrorMessage clears it. getFields returns the current parsed field tree including the $sub containers for nested objects. And vm gives you the reactive view-model, which is described as the model, the fields and the error, for advanced consumers and option-callback access.

The events are four. update:modelValue is emitted for v-model whenever a field value changes. change fires when a change to an element's value is committed by the user, which is a different moment from the model updating. submit fires when the form is submitted and passes validation. And invalid fires when a submittable element has been checked and does not satisfy its constraints.

The distinction between change and update:modelValue is the one to notice. The model updates as you type, while change fires on commit. A component that needs to avoid firing an autosave on every keystroke binds to change; one that needs live preview binds to the model. Having both is the difference between a form library that assumes and one that gives you the choice.

The vm property and the getFields method are the two escape hatches, and they are what you would use to build something the library does not provide. Since the option callbacks receive the view-model, a custom widget can read the whole form state, not just its own field.

The repository structure supports the picture of a component library rather than an application: src/ for the source, test/ and a vitest.config.ts for the tests, example/ with App.vue, components/ and a schema/ directory, docs/, static/, a screenshot.png, and a build stack of vite with postcss, biome for linting and formatting, babel, coveralls for coverage, and separate tsconfig files for the source and the node tooling.

## Conclusion

Adopt vue-json-ui-editor if you generate forms from a JSON Schema in Vue 3 and want to keep control of what the controls look like, since the library ships no UI library and hands the choice to your application through a static registry with per-instance and per-field overrides. Do not expect it to hide the awkward parts of element-plus, because the label registration has to return a label prop explicitly, since el-form-item reads its text from the prop rather than the slot, and a missed detail there produces a form with invisible labels. Before you commit, pick the branch that matches your Vue version, because the 3.x, 2.x and 1.x lines are maintained separately, and read the props and methods list rather than only the example, because getFields, vm and the setErrorMessage pair are what you will reach for once the simple case stops being simple.

## FAQ

### What is vue-json-ui-editor?

It is a Vue 3 JSON Schema based form editor component, for editing JSON in UI form with a JSON Schema, with full TypeScript support. It ships with no UI library of its own and renders with native HTML elements by default, and the primary language is TypeScript with an MIT licence and the homepage at jsonui.js.org.

### How do I install vue-json-ui-editor and which branch should I use?

Run npm install vue-json-ui-editor or pnpm add vue-json-ui-editor. The main branch is the 3.x line for Vue 3 with element-plus, the master branch is the 2.x line for Vue 2 with element-ui in maintenance, and the v1 branch is the legacy 1.x line, so pick the branch that matches your Vue version.

### How do I use element-plus with vue-json-ui-editor?

Register the components yourself through the static JsonEditor.setComponent API, passing a type such as text, form, label, select or error, a component or native tag name, and an optional option object or a factory callback that receives the view-model, the field and the item. The example shows el-input for text, el-form for form and el-alert for error.

### Can two editors on the same page use different UI components?

Yes. The components prop is a per-instance override merged over the global registry, so multiple json-editor instances on one page can each use a different UI library without affecting each other. The keys are element types and the values may be a full component and option config or a shorthand component name or object.

### What methods and events does the json-editor component expose?

Through a template ref it exposes input, form, checkValidity, validate, reset, setErrorMessage, clearErrorMessage, getFields and vm. The events are update:modelValue for the model binding, change when a value is committed, submit when the form passes validation, and invalid when a checked element does not satisfy its constraints.

## Sources

- [Issues](https://github.com/yourtion/vue-json-ui-editor/issues)
- [License: MIT](https://github.com/yourtion/vue-json-ui-editor/blob/main/LICENSE)
- [Project website](http://jsonui.js.org/)
- [README](https://github.com/yourtion/vue-json-ui-editor/blob/main/README.md)
- [yourtion/vue-json-ui-editor on GitHub](https://github.com/yourtion/vue-json-ui-editor)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/yourtion-vue-json-ui-editor
