DEV Community

Cover image for JSON Schema to <form> in 18 Lines of vanilla JS
Rémy F.
Rémy F.

Posted on

1

JSON Schema to <form> in 18 Lines of vanilla JS

Using https://dev.to/yne/a-one-liner-for-createelement-with-attributes-and-children-55nh
The following snippet convert any JSON Schema to DOM elements.

const el = (tag, props = {}, ch = [], attrs = {}) => ch.reduce((e, c) => (e.appendChild(c), e), Object.entries(attrs).reduce((e, [k, v]) => (e.setAttribute(k, v), e), Object.assign(document.createElement(tag), Object.fromEntries(Object.entries(props).filter(([key, value]) => value !== undefined)))));

function schema2dom(item, name = "", parent = {}) {
    let ul, total; // this total <input> track if we stay in minItems/maxItems
    if (item.type == "array") return [ul = el("ul", {}, [el("li", {}, [el("button", {
        type: "button", innerText: "Add", onclick: () => ul.insertAdjacentElement("beforeend", el("li", { ariaRowIndex: total.ariaRowIndex = ++total.value }, [
            el("button", { type: "button", innerText: "Remove", onclick() { total.ariaRowIndex = --total.value; this.parentElement.remove() } }),
            ...schema2dom(item.items, name, parent.required?.includes(name))
        ]))
    }), total = el("input", { type: "number", ariaRowIndex: 0, value: 0, min: item.minItems, max: item.maxItems, oninput() { this.value = this.ariaRowIndex } })])])];
    if (item.type == "object") return [el("dl", {  }, Object.entries(item.properties).map(([name, v]) => [
        el("dt", { innerText: v.title || name, title: v.description || '' }),
        el("dd", {}, schema2dom(v, name, item)),// "additionalProperties", propertyNames+pattern, minProperties maxProperties
    ]).flat())];
    const common = { name, title:item.description||parent.description, required: parent.required?.includes(name), minLength: item.minLength, maxLength: item.maxLength, pattern: item.pattern, min: item.minimum, max: item.maximum };
    if (item.enum) return [el("select", common, item.enum.map((v,i) => el("option", { innerText: v === Object(v) ? JSON.stringify(v) : v, value: JSON.stringify(v), title:(item.markdownEnumDescriptions||item.enumDescriptions||[])[i]})))];
    const datalist = [el("datalist", { id: `list_${Math.random()}` }, item.examples?.map(ex => el("option", { innerText: ex, title: JSON.stringify(ex) })))];
    if (item.type == "boolean") return [el("input", { type: "checkbox", checked: item.default, ...common }), ...datalist];
    if (item.type == "integer") return [el("input", { type: "number", value: item.default, step: item.multipleOf || 1, ...common }), ...datalist];
    if (item.type == "number") return [el("input", { type: "number", value: item.default, step: item.multipleOf, ...common }), ...datalist];
    if (item.type == "string") return [el("input", { type: "text", value: item.default, ...common }, [], { list: datalist?.[0].id }), ...datalist];
    return []; // item.type == "null"
}
Enter fullscreen mode Exit fullscreen mode

For example:

schema2dom({type:"object",properties:{n:{type:"number"},s:{type:"string"}}})
Enter fullscreen mode Exit fullscreen mode

Will return 2 fields: a <input type=number> plus a <input type=text>

Limitation

  • examples can't be array or object they will be toString() and put as value of
  • Object are non-extensible (see additionalProperties and associated propertyNames,pattern,minProperties,maxProperties.

Serialization

Simple Schema generated

can be serialized using DataForm():
const obj = Object.fromEntries((new FormData(myForm)).entries())

However, nested list/objects require a specific serialization:

function serialise(el) {
    if (el.tagName == 'DL') return Object.fromEntries([...el.children].reduce((acc, _, i, ch) => i % 2 ? acc : [...acc, [ch[i].innerText, serialise(ch[i + 1].children[0])]], []))
    if (el.tagName == 'UL') return [...el.children].slice(1).map(li => serialise(li.children[1]))
    if (el.tagName == 'SELECT') return JSON.parse(el.value);
    if (el.type == 'checkbox') return el.checked;
    if (el.type == 'number') return el.valueAsNumber;
    if (el.type == 'text') return el.value;
    return null;
}

Style

No style are required, but a grid display could help.

dl,ul>li {
    display: grid;
    grid-template-columns: 1fr 5fr;
}
dd { display: contents; }
dl, ul {
    padding: 0;
    margin: 0;
}

Top comments (0)

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay