Constructing JSON in a form field in a declarative custom node

Hi all,

I’m building a custom node that I would prefer to keep declarative rather than programmatic if possible. I need to talk to an API that requires a x-www-form-urlencoded body with a single parameter that contains a JSON value. So in cURL it would look like e.g.

--data-urlencode 'major={"name": "Advanced Foobar","code": "Foo1","active": false}'

Could I build this request in a declarative custom node? If it were raw JSON it would be easy to just use routing.send.property on the individual fields, but I’m not sure how I would construct the JSON from three different fields when it’s inside a form value.

Hey @cacl-dis, we’ve built a few declarative custom nodes and ran into something similar, you can define individual fields (name, code, active) as normal input properties, then use a computed field that assembles those into a JSON string using expression.

Here’s a pattern that works inside properties:

{
  displayName: 'Major',
  name: 'major',
  type: 'string',
  default: '',
  description: 'JSON stringified major data',
  required: true,
  displayOptions: {
    show: {
      // Optional: only show when other fields are filled
    },
  },
  expression: true,
}

And in routing.send.body:

{
  type: 'form-urlencoded',
  property: 'major',
  value: '={{ JSON.stringify({ name: $json.name, code: $json.code, active: $json.active }) }}'
}

:brain: Tip:

Even though it’s one parameter, the key (major) is preserved, and the API should receive:

major={"name":"Advanced Foobar","code":"Foo1","active":false}

This keeps your node declarative and avoids dropping into execute() unless necessary.

Best,
Team Hashlogics
:link: https://hashlogics.com

1 Like

Hey @Hashlogics

I have gotten this to work with static values, but I’m having trouble with the computed values. $json is an empty object, and I can’t seem to find any other way to reference those other fields.

This may be because I can’t add expression: true to my fields, as it’s not a known property for INodeProperties.

I’d share a working approach that uses a custom node with execute() to programmatically construct your x-www-form-urlencoded body including the embedded JSON.

Since you’re aiming for a declarative solution, unfortunately INodeProperties doesn’t support dynamic JSON composition within a single field natively (and expression: true isn’t valid there either). But if you’re open to using a minimal programmatic node, this keeps things clean while giving you full control over the structure.


:white_check_mark: What This Node Does:

  • Accepts three input fields: name, code, and active
  • Combines them into a JSON object
  • Encodes that JSON object into a x-www-form-urlencoded body under a single field: major
  • Outputs the result as a body string and appropriate headers for further HTTP use

:wrench: Full Custom Node Code (TypeScript):

import { IExecuteFunctions } from 'n8n-workflow';
import { INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';

export class JsonFormEncodeNode implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'JSON Form Encode',
    name: 'jsonFormEncode',
    group: ['transform'],
    version: 1,
    description: 'Send x-www-form-urlencoded with embedded JSON in a single param',
    defaults: {
      name: 'JSON Form Encode',
    },
    inputs: ['main'],
    outputs: ['main'],
    properties: [
      {
        displayName: 'Name',
        name: 'name',
        type: 'string',
        default: '',
      },
      {
        displayName: 'Code',
        name: 'code',
        type: 'string',
        default: '',
      },
      {
        displayName: 'Active',
        name: 'active',
        type: 'boolean',
        default: false,
      },
    ],
  };

  async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    const returnData: INodeExecutionData[] = [];

    for (let i = 0; i < items.length; i++) {
      const name = this.getNodeParameter('name', i) as string;
      const code = this.getNodeParameter('code', i) as string;
      const active = this.getNodeParameter('active', i) as boolean;

      const major = { name, code, active };
      const formEncoded = new URLSearchParams();
      formEncoded.append('major', JSON.stringify(major));

      returnData.push({
        json: {
          body: formEncoded.toString(),
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
          },
        },
      });
    }

    return [returnData];
  }
}

:outbox_tray: Example Output:

The body value from this node will look like:

major={"name":"Advanced Foobar","code":"Foo1","active":false}

And it includes a Content-Type: application/x-www-form-urlencoded header so you can pipe this into an HTTP Request node or further downstream.


If you’re not building custom nodes or want help packaging this, I can share a .zip or GitHub snippet too. Let me know!

Hope this clears it up and helps you move forward :victory_hand:

1 Like