# What do you guys think about writting nodes using json schemas?

**URL:** https://discourse.nodered.org/t/what-do-you-guys-think-about-writting-nodes-using-json-schemas/96936
**Category:** Developing Nodes
**Created:** [6 May 2025 23:46 UTC](https://discourse.nodered.org/t/what-do-you-guys-think-about-writting-nodes-using-json-schemas/96936 "2025-05-06T23:46:59Z")
**Posts on this page:** 4
**Page:** 1

<div class="post-metadata">

### Author: ![AllanOricil](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/allanoricil/32/106911_2.png) [@AllanOricil](https://discourse.nodered.org/u/AllanOricil)
#### Post date: [6 May 2025 23:46 UTC](https://discourse.nodered.org/t/what-do-you-guys-think-about-writting-nodes-using-json-schemas/96936/1 "2025-05-06T23:46:59Z")

</div>

The framework I'm creating uses json schemas (draft v7) as the source of truth for configs (defaults), credentials, input and outputs messages. This is the folder structure of a single node.

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/7/c/7c056c66db122b66db84d23b3fdbe3121830a19a.png)

This is an example of a Node that uses json schemas for configs, credentials, input and output messages

> <https://github.com/AllanOricil/node-red-vue-inputs-experiments/blob/main/src/nodes/your-node/schemas.ts>

> <https://github.com/AllanOricil/node-red-vue-inputs-experiments/blob/main/src/nodes/your-node/server/index.ts>

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/0/1/01f3d21f8aabc1ed074ddeae5b55e21ce8859571.png)

This an example of Config Node that use a schema for Config props only

> <https://github.com/AllanOricil/node-red-vue-inputs-experiments/blob/main/src/nodes/remote-server/server/index.ts>

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/f/c/fc2e5e3f2f33086115877d022e8c178d7d6c305f.png)

In this framework you can use TypeBox to write your schemas with typechecking or just write an object that complies with json schema draft v7. The only additional keyword I had to add is "nodeType", which allows developers to define the node type of a config node.

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/d/1/d1b8c3360e57c5bad0f5baca10b106f1a414843f.png)

Intelisense is based on the types you create from your schemas

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/3/1/31536b908b2c1a6ef1456f4d328a381f72d9cae5.png)

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/c/5/c5fabe1ccd45cd3c87c81079fdb983e6cd996b7a.png)  
 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/e/8/e862cbe212b41071d811986f2b064c79c1ce19d3.png)  
 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/6/f/6fabb0c83af217c5d680ee9a27d69d70b858096c.png)

Validations run in the server using the same schemas

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/4/4/44d2ac348afb26453d79e58cb0bfc929cc86273b.png)

The client side of your node is now reduced to a simple Vue component

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/d/e/deba56ae06b44f4e45ea373d2c2a6594b75a09d1.png)

And the registration is much simpler than ever before. You no longer have to redefine defaults or credentials. The source of thruth is also your schemas

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/7/c/7c9872d2449becbcaa55e4c18bd7d69c829ecb42.png)

If you want to use other node-red node props you can still do it

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/2/4/2427c7bad137122c47ffec138d9d23aeb03a3772.png)

While writting your node forms you can use the same built in node-red inputs you are used to, but with a much simpler api.

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/5/4/54592f66bb47f12cca79a2cd075f14bf50028033.png)

Typed Input

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/8/9/89cdce3ea9191535ad8cd241d4792a3d667b663d.png)

Config Input

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/2/1/2106ab95f5ad536904a2c92e1247dd32c1aee1ee.png)

Select and MultiSelect Input

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/0/5/052106c750d396dd391f0949cfef85e05fede7ec.png)

Editor Input

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/e/6/e6b713f1b60e8bdcf2489546518ab98fd407095c.png)

To avoid breaking the editor all these inputs are being mounted in the dom using node-red apis (jquery widgets)

sourcemaps out of the box in dev builds

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/6/d/6d7858ba878afbef4b59f7ea48c8b7ce019443f6.png)

vue devtools

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/4/b/4b6de6fd5a41643c8c897edc98bd9ee17fdf9851.png)

you can choose to disable the save button on error

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/f/4/f430170e35aa165d3312b213635d4cffee760c4f.png)  
 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/c/5/c57be1b0256140a5dbecd5bcfdf6d56494759f24.png)  
 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/f/6/f63ea91652664c13aceb3644c63b8d1670a10959.png)

---

<div class="post-metadata">

### Author: ![AllanOricil](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/allanoricil/32/106911_2.png) [@AllanOricil](https://discourse.nodered.org/u/AllanOricil)
#### Post date: [12 May 2025 15:59 UTC](https://discourse.nodered.org/t/what-do-you-guys-think-about-writting-nodes-using-json-schemas/96936/2 "2025-05-12T15:59:13Z")

</div>

I think I have finaly reached a final design for the framework. You can check it out in the following link

> **[GitHub - AllanOricil/node-red-vue-inputs-experiments: Write Node-RED nodes using Vue and Typescript](https://github.com/AllanOricil/node-red-vue-inputs-experiments)**
>
> Write Node-RED nodes using Vue and Typescript

The following is an example of an IONode. IONodes are the ones we can drag and drop in the canvas.

`src/nodes/your-node/server/index.ts`

```ts
import { Static } from "@sinclair/typebox";
import {
  CloseDoneFunction,
  InputDoneFunction,
  IONode,
  IONodeValidations,
  SendFunction,
} from "../../../core/server/nodes";
import RemoteServerConfigNode from "../../remote-server/server";
import {
  ConfigsSchema,
  CredentialsSchema,
  InputMessageSchema,
  OutputMessageSchema,
} from "../schemas";

export type YourNodeConfigs = Static<typeof ConfigsSchema>;
export type YourNodeCredentials = Static<typeof CredentialsSchema>;
export type YourNodeInputMessage = Static<typeof InputMessageSchema>;
export type YourNodeOutputMessage = Static<typeof OutputMessageSchema>;

export default class YourNode extends IONode<
  YourNodeConfigs,
  YourNodeCredentials,
  YourNodeInputMessage,
  YourNodeOutputMessage
> {
  static override validations: IONodeValidations = {
    configs: ConfigsSchema,
    credentials: CredentialsSchema,
    input: InputMessageSchema,
    outputs: OutputMessageSchema,
  };

  static override async init() {
    console.log("testing your node init");
  }

  async onInput(
    msg: {
      payload?: string | undefined;
      topic?: string | undefined;
      _msgid?: string | undefined;
    } & { myVariable?: string | undefined },
    send: SendFunction<
      {
        payload?: string | undefined;
        topic?: string | undefined;
        _msgid?: string | undefined;
      } & { originalType: "string" | "number"; processedTime: number }
    >,
    done: InputDoneFunction
  ): Promise<void> {
    console.log(this);
    console.log(msg);

    const server = IONode.getNode<RemoteServerConfigNode>(
      this.configs.remoteServer
    );

    console.log(server?.users);
  }

  async onClose(removed: boolean, done: CloseDoneFunction): Promise<void> {
    console.log("removing node");
    done();
  }
}

```

`src/nodes/your-node/client/index.ts`

```ts
import { defineNode } from "../../../core/client";
import component from "./Form.vue";

export default defineNode({
  category: "function",
  color: "#FFFFFF",
  inputs: 1,
  outputs: 1,
  icon: "vue.png",
  form: {
    component,
    disableSaveButtonOnError: true,
  }
});

```

`src/nodes/your-node/client/Form.vue`

```html
<template>
  <div>
    <div class="form-row">
      <label><i class="fa fa-tag"></i> Username</label>
      <NodeRedInput
        v-model:value="node.credentials.username"
        :error="errors['node.credentials.username']"
      />
    </div>

    <div class="form-row">
      <label><i class="fa fa-tag"></i> Password</label>
      <NodeRedInput
        v-model:value="node.credentials.password"
        type="password"
        :error="errors['node.credentials.password']"
      />
    </div>

    <div class="form-row">
      <label><i class="fa fa-tag"></i> Password 2</label>
      <NodeRedInput
        v-model:value="node.credentials.password2"
        type="password"
        :error="errors['node.credentials.password2']"
      />
    </div>

    <div class="form-row">
      <label>Typed Input</label>
      <NodeRedTypedInput
        v-model:value="node.myProperty"
        :types="types"
        :error="errors['node.myProperty']"
      />
    </div>

    <div class="form-row">
      <label>Typed Input 2</label>
      <NodeRedTypedInput
        v-model:value="node.myProperty2"
        :error="errors['node.myProperty2']"
      />
    </div>

    <div class="form-row">
      <label>Config Input</label>
      <NodeRedConfigInput
        v-model:value="node.remoteServer"
        type="remote-server"
        :error="errors['node.remoteServer']"
      />
    </div>

    <div class="form-row">
      <label>Config Input</label>
      <NodeRedConfigInput
        v-model:value="node.anotherRemoteServer"
        type="remote-server"
        :error="errors['node.anotherRemoteServer']"
      />
    </div>

    <div class="form-row">
      <label>Select Input</label>
      <NodeRedSelectInput
        v-model:value="node.country"
        :options="countries"
        :error="errors['node.country']"
      />
    </div>

    <div class="form-row">
      <label>MultiSelect Input</label>
      <NodeRedSelectInput
        v-model:value="node.fruit"
        :options="fruits"
        multiple
        :error="errors['node.fruit']"
      />
    </div>

    <div class="form-row">
      <label>Select Input</label>
      <NodeRedSelectInput
        v-model:value="node.number"
        :options="numbers"
        :error="errors['node.number']"
      />
    </div>

    <div class="form-row">
      <label>Select Input</label>
      <NodeRedSelectInput
        v-model:value="node.object"
        :options="objects"
        multiple
        :error="errors['node.object']"
      />
    </div>

    <div class="form-row">
      <label>Select Input</label>
      <NodeRedSelectInput
        v-model:value="node.array"
        :options="arrays"
        :error="errors['node.array']"
      />
    </div>

    <div class="form-row">
      <label>Editor with default height 200px and JSON</label>
      <NodeRedEditorInput
        v-model:value="node.jsontest"
        :error="errors['node.jsontest']"
      />
    </div>

    <div class="form-row">
      <label>Editor with custom height and CSS</label>
      <NodeRedEditorInput
        v-model:value="node.csstest"
        language="css"
        style="height: 100px"
        :error="errors['node.csstest']"
      />
    </div>

    <!-- NOTE: this is loaded from the resources/{pkg.name} folder -->
    <img src="/vue.png" />

    <!-- NOTE: this is added to the bundled .js -->
    <img :src="logo" alt="Logo" />
  </div>
</template>

<script>
import logo from "../../../assets/vue.png";
export default {
  name: "NodeRedNodeForm",
  props: {
    node: {
      type: Object,
      required: true,
    },
    errors: {
      type: Object,
      default: () => ({}),
    },
  },
  data() {
    return {
      logo,
      types: ["str", "msg", "node"],
      countries: [
        { value: "usa", label: "usa" },
        { value: "argentina", label: "argentina" },
        { value: "brasil", label: "brasil" },
      ],
      fruits: [
        { value: "apple", label: "apple" },
        { value: "melon", label: "melon" },
        { value: "raspberry", label: "raspberry" },
      ],
      numbers: [
        { value: "1", label: "1" },
        { value: "2", label: "2" },
        { value: "3", label: "3" },
      ],
      objects: [
        { value: JSON.stringify({ test: "a" }), label: "a" },
        { value: JSON.stringify({ test: "b" }), label: "b" },
        { value: JSON.stringify({ test: "c" }), label: "c" },
      ],
      arrays: [
        { value: JSON.stringify(["a"]), label: "a" },
        { value: JSON.stringify(["b"]), label: "b" },
        { value: JSON.stringify(["c"]), label: "c" },
      ],
    };
  },
  mounted() {
    console.log("MOUNTED");
    console.log(this.logo);
  },
};
</script>

```

`src/nodes/your-node/schemas.ts`

```ts
import { Type, Static } from "@sinclair/typebox";
import {
  IONodeConfigsSchema,
  TypedInputSchema,
  MessageSchema,
} from "../../core/schemas";

const ConfigsSchema = Type.Object(
  {
    ...IONodeConfigsSchema.properties,
    name: Type.String({ default: "your-node" }),
    myProperty: TypedInputSchema,
    myProperty2: TypedInputSchema,
    remoteServer: Type.String({ nodeType: "remote-server" }),
    anotherRemoteServer: Type.Optional(
      Type.String({ nodeType: "remote-server" })
    ),
    country: Type.String({ default: "brazil" }),
    fruit: Type.Array(Type.String(), { default: ["apple", "melon"] }),
    number: Type.String({ default: "1" }),
    object: Type.Array(Type.String(), {
      default: [JSON.stringify({ test: "a" }), JSON.stringify({ test: "b" })],
    }),
    array: Type.String({
      default: '["a"]',
    }),
    jsontest: Type.String({ default: "" }),
    csstest: Type.String({ default: "" }),
  },
  {
    $id: "YourNodeConfigsSchema",
  }
);

const CredentialsSchema = Type.Object(
  {
    password: Type.Optional(
      Type.String({
        default: "",
        minLength: 8,
        maxLength: 20,
        pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]+$/.source,
        format: "password",
      })
    ),
    password2: Type.Optional(
      Type.String({
        default: "",
        pattern:
          /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
            .source,
        format: "password",
      })
    ),
    username: Type.Optional(
      Type.String({ default: "", maxLength: 10, minLength: 5 })
    ),
  },
  {
    $id: "YourNodeCredentialsSchema",
  }
);

const InputMessageSchema = Type.Intersect(
  [
    MessageSchema,
    Type.Object({
      myVariable: Type.Optional(Type.String()),
    }),
  ],
  {
    $id: "YourNodeInputMessageSchema",
  }
);

const OutputMessageSchema = Type.Intersect(
  [
    MessageSchema,
    Type.Object({
      originalType: Type.Union([
        Type.Literal("string"),
        Type.Literal("number"),
      ]),
      processedTime: Type.Number(),
    }),
    Type.Unknown(),
  ],
  { $id: "YourNodeOutputMessageSchema" }
);

export {
  ConfigsSchema,
  CredentialsSchema,
  InputMessageSchema,
  OutputMessageSchema,
};

```

The only part that is a little bit complicated is defining JSON Schemas using TypeBox. However, if you don't like it, you can define the JSON Schema by hand, using an object.

---

<div class="post-metadata">

### Author: ![AllanOricil](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/allanoricil/32/106911_2.png) [@AllanOricil](https://discourse.nodered.org/u/AllanOricil)
#### Post date: [12 May 2025 16:13 UTC](https://discourse.nodered.org/t/what-do-you-guys-think-about-writting-nodes-using-json-schemas/96936/3 "2025-05-12T16:13:05Z")

</div>

And this is how you create a ConfigNode

`src/nodes/remote-server/server/index.ts`

```ts
import { Static } from "@sinclair/typebox";
import { ConfigNode, ConfigNodeValidations } from "../../../core/server/nodes";
import { ConfigsSchema } from "../schemas";

export type RemoteServerConfigs = Static<typeof ConfigsSchema>;

export default class RemoteServerConfigNode extends ConfigNode<RemoteServerConfigs> {
  static override validations: ConfigNodeValidations = {
    configs: ConfigsSchema,
  };

  // NOTE: run only once when node type is registered
  static override init() {
    console.log("server-node");
  }
}

```

`src/nodes/remote-server/client/index.ts`

```ts
import { defineNode } from "../../../core/client";
import component from "./Form.vue";

export default defineNode({
  category: "config",
  color: "#a6bbcf",
  form: {
    component,
    disableSaveButtonOnError: true,
  },
});

```

`src/nodes/remote-server/client/Form.vue`

```ts
<template>
  <div>
    <div class="form-row">
      <label><i class="fa fa-tag"></i> Name</label>
      <NodeRedInput
        v-model:value="node.name"
        :error="errors['node.name']"
        placeholder="name"
      />
    </div>
    <div class="form-row">
      <label><i class="fa fa-tag"></i> Hostname</label>
      <NodeRedInput
        v-model:value="node.host"
        :error="errors['node.host']"
        placeholder="hostname"
      />
    </div>
  </div>
</template>

<script>
export default {
  name: "NodeRedNodeForm",
  props: {
    node: {
      type: Object,
      required: true,
    },
    errors: {
      type: Object,
      default: () => ({}),
    },
  },
};
</script>

```

`src/nodes/remote-server/schemas.ts`

```ts
import { Type } from "@sinclair/typebox";
import { ConfigNodeConfigsSchema } from "../../core/schemas";

const ConfigsSchema = Type.Object(
  {
    ...ConfigNodeConfigsSchema.properties,
    name: Type.String({ default: "remote-server", minLength: 10 }),
    host: Type.String({ default: "localhost" }),
  },
  {
    $id: "RemoteServerConfigsSchema",
  }
);

export { ConfigsSchema };

```

---

<div class="post-metadata">

### Author: ![system](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/1X/d073cd938eafa2e558d7c2cd59003b3ef4963033.png) [@system](https://discourse.nodered.org/u/system)
#### Post date: [11 July 2025 16:13 UTC](https://discourse.nodered.org/t/what-do-you-guys-think-about-writting-nodes-using-json-schemas/96936/4 "2025-07-11T16:13:54Z")

</div>

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.
