AutomationFlowsData & Sheets › Validate JSON and CSV Import Data via Webhook with Configurable Rules

Validate JSON and CSV Import Data via Webhook with Configurable Rules

ByFlorian Eiche @jagged on n8n.io

Send a POST request with a JSON array of records and your validation rules. The workflow checks every field in every row and returns a structured report showing valid/invalid rows with specific error messages.

Webhook trigger★★★★☆ complexity9 nodes
Data & Sheets Trigger: Webhook Nodes: 9 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #13999 — we link there as the canonical source.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "63xAsYgwMTW1oqMe",
  "name": "Validate CSV and JSON import data with configurable rules via webhook",
  "tags": [],
  "nodes": [
    {
      "id": "450b896a-7d82-45d9-9a11-4b1e67a8e497",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        -128
      ],
      "parameters": {
        "color": 4,
        "width": 800,
        "height": 588,
        "content": "## Validate CSV/JSON Import Data with Configurable Rules\n\nThis workflow provides a **reusable data validation API endpoint** for your import pipelines. Send any JSON array of records along with validation rules, and get back a detailed report showing which rows passed and which failed, with specific error messages per field.\n\n### Who is this for?\nOperations teams, data engineers, or anyone importing data into ERP, CRM, databases, or spreadsheets who needs to catch errors **before** they enter the system.\n\n### How it works\n1. Send a POST request with `data` (array of records) and `rules` (validation config)\n2. The workflow validates every field in every row against your rules\n3. Returns a structured JSON report: total/valid/invalid rows + detailed errors\n\n### Supported validation rules\n`required`, `type` (string, number, email, date, url, boolean), `min`, `max`, `minLength`, `maxLength`, `regex`, `enum`, `dateFormat`\n\n### Setup\n1. Activate the workflow\n2. Send a POST request to the webhook URL\n3. Optionally configure default rules in the **Set Default Rules** node\n\n**Author:** Florian Eiche, [eiche-digital.de](https://eiche-digital.de)"
      },
      "typeVersion": 1
    },
    {
      "id": "0143af87-c46e-4707-8318-fed250e86739",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        480
      ],
      "parameters": {
        "width": 280,
        "height": 328,
        "content": "### Step 1: Receive Data\nPOST request with JSON body:\n```json\n{\n  \"data\": [{...}, {...}],\n  \"rules\": {\n    \"fieldName\": {\n      \"required\": true,\n      \"type\": \"email\"\n    }\n  }\n}\n```\nRules in the request override the defaults."
      },
      "typeVersion": 1
    },
    {
      "id": "6b3086af-a198-426b-bafc-421d8ac13068",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        320,
        480
      ],
      "parameters": {
        "width": 280,
        "height": 328,
        "content": "### Step 2: Default Rules\nConfigure fallback validation rules here. These are used when the request body does not include a `rules` object.\n\nEdit the JSON in the **Set Default Rules** node to match your data structure."
      },
      "typeVersion": 1
    },
    {
      "id": "ba474ce4-e4da-47e4-a912-5a320dc60eb5",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        480
      ],
      "parameters": {
        "width": 300,
        "height": 328,
        "content": "### Step 3: Validate\nThe Code node checks every row against the rules and collects all errors.\n\nSupported checks:\n- `required`\n- `type`: string, number, email, date, url, boolean\n- `min` / `max` (numbers)\n- `minLength` / `maxLength`\n- `regex` (custom pattern)\n- `enum` (allowed values)\n- `dateFormat` (YYYY-MM-DD, DD.MM.YYYY, MM/DD/YYYY)"
      },
      "typeVersion": 1
    },
    {
      "id": "761e962f-7e78-4418-8bcf-814c0e67c8cc",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        992,
        416
      ],
      "parameters": {
        "width": 296,
        "height": 392,
        "content": "### Step 4: Response\nReturns a JSON report:\n```json\n{\n  \"valid\": false,\n  \"summary\": {\n    \"totalRows\": 3,\n    \"validRows\": 1,\n    \"invalidRows\": 2,\n    \"totalErrors\": 4\n  },\n  \"errors\": [\n    {\n      \"row\": 2,\n      \"field\": \"email\",\n      \"value\": \"invalid\",\n      \"rule\": \"type:email\",\n      \"message\": \"...\"\n    }\n  ]\n}\n```"
      },
      "typeVersion": 1
    },
    {
      "id": "9cba0c0c-f991-4113-b267-3577c6a77698",
      "name": "Receive Data",
      "type": "n8n-nodes-base.webhook",
      "position": [
        112,
        848
      ],
      "parameters": {
        "path": "validate-data",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "29a55404-f6ac-47e3-870c-e9d4edde4b9a",
      "name": "Set Default Rules",
      "type": "n8n-nodes-base.set",
      "position": [
        432,
        848
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "{\n  \"rules\": {\n    \"name\": {\n      \"required\": true,\n      \"type\": \"string\",\n      \"minLength\": 2,\n      \"maxLength\": 100\n    },\n    \"email\": {\n      \"required\": true,\n      \"type\": \"email\"\n    },\n    \"age\": {\n      \"required\": false,\n      \"type\": \"number\",\n      \"min\": 0,\n      \"max\": 150\n    },\n    \"status\": {\n      \"required\": true,\n      \"type\": \"string\",\n      \"enum\": [\"active\", \"inactive\", \"pending\"]\n    },\n    \"website\": {\n      \"required\": false,\n      \"type\": \"url\"\n    },\n    \"joinDate\": {\n      \"required\": false,\n      \"type\": \"date\",\n      \"dateFormat\": \"YYYY-MM-DD\"\n    }\n  }\n}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "38d23008-b445-4885-be49-c76b0478e877",
      "name": "Validate Data",
      "type": "n8n-nodes-base.code",
      "position": [
        752,
        848
      ],
      "parameters": {
        "jsCode": "// Get input data and rules\nconst input = $('Receive Data').first().json.body;\nconst defaults = $('Set Default Rules').first().json;\n\nconst data = input.data;\nconst rules = input.rules || defaults.rules || {};\n\n// Validate input\nif (!data || !Array.isArray(data)) {\n  return [{\n    json: {\n      valid: false,\n      error: 'Invalid input: \"data\" must be an array of objects.',\n      summary: { totalRows: 0, validRows: 0, invalidRows: 0, totalErrors: 1 },\n      errors: []\n    }\n  }];\n}\n\nif (!rules || Object.keys(rules).length === 0) {\n  return [{\n    json: {\n      valid: false,\n      error: 'No validation rules provided. Send rules in the request body or configure them in the Set Default Rules node.',\n      summary: { totalRows: data.length, validRows: 0, invalidRows: 0, totalErrors: 1 },\n      errors: []\n    }\n  }];\n}\n\n// Validation helpers\nconst isPresent = (v) => v !== null && v !== undefined && v !== '';\nconst isEmail = (v) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(String(v));\nconst isNumber = (v) => !isNaN(Number(v)) && String(v).trim() !== '';\nconst isUrl = (v) => /^https?:\\/\\/.+\\..+/.test(String(v));\nconst isBool = (v) => ['true','false','0','1','yes','no'].includes(String(v).toLowerCase());\n\nfunction isValidDate(v, fmt) {\n  if (fmt === 'YYYY-MM-DD') return /^\\d{4}-\\d{2}-\\d{2}$/.test(v);\n  if (fmt === 'DD.MM.YYYY') return /^\\d{2}\\.\\d{2}\\.\\d{4}$/.test(v);\n  if (fmt === 'MM/DD/YYYY') return /^\\d{2}\\/\\d{2}\\/\\d{4}$/.test(v);\n  return !isNaN(Date.parse(v));\n}\n\nconst errors = [];\nlet validRows = 0;\nlet invalidRows = 0;\n\nfor (let i = 0; i < data.length; i++) {\n  const row = data[i];\n  let rowHasError = false;\n\n  for (const [field, fr] of Object.entries(rules)) {\n    const value = row[field];\n\n    // Required check\n    if (fr.required && !isPresent(value)) {\n      errors.push({\n        row: i + 1, field, value: value ?? null,\n        rule: 'required',\n        message: `Field '${field}' is required`\n      });\n      rowHasError = true;\n      continue;\n    }\n\n    // Skip further checks if empty and not required\n    if (!isPresent(value)) continue;\n\n    // Type checks\n    if (fr.type) {\n      let typeValid = true;\n      let typeLabel = fr.type;\n\n      switch (fr.type) {\n        case 'string':\n          typeValid = typeof value === 'string';\n          break;\n        case 'number':\n          typeValid = isNumber(value);\n          break;\n        case 'email':\n          typeValid = isEmail(value);\n          break;\n        case 'url':\n          typeValid = isUrl(value);\n          break;\n        case 'boolean':\n          typeValid = isBool(value);\n          break;\n        case 'date':\n          typeValid = isValidDate(value, fr.dateFormat);\n          typeLabel = fr.dateFormat ? `date (${fr.dateFormat})` : 'date';\n          break;\n      }\n\n      if (!typeValid) {\n        errors.push({\n          row: i + 1, field, value,\n          rule: `type:${fr.type}`,\n          message: `Field '${field}' must be a valid ${typeLabel}`\n        });\n        rowHasError = true;\n      }\n    }\n\n    // String length checks\n    if (fr.minLength !== undefined && String(value).length < fr.minLength) {\n      errors.push({\n        row: i + 1, field, value,\n        rule: 'minLength',\n        message: `Field '${field}' must be at least ${fr.minLength} characters`\n      });\n      rowHasError = true;\n    }\n\n    if (fr.maxLength !== undefined && String(value).length > fr.maxLength) {\n      errors.push({\n        row: i + 1, field, value,\n        rule: 'maxLength',\n        message: `Field '${field}' must be at most ${fr.maxLength} characters`\n      });\n      rowHasError = true;\n    }\n\n    // Numeric range checks\n    if (fr.min !== undefined && isNumber(value) && Number(value) < fr.min) {\n      errors.push({\n        row: i + 1, field, value,\n        rule: 'min',\n        message: `Field '${field}' must be >= ${fr.min}`\n      });\n      rowHasError = true;\n    }\n\n    if (fr.max !== undefined && isNumber(value) && Number(value) > fr.max) {\n      errors.push({\n        row: i + 1, field, value,\n        rule: 'max',\n        message: `Field '${field}' must be <= ${fr.max}`\n      });\n      rowHasError = true;\n    }\n\n    // Regex check\n    if (fr.regex && !new RegExp(fr.regex).test(String(value))) {\n      errors.push({\n        row: i + 1, field, value,\n        rule: 'regex',\n        message: `Field '${field}' does not match pattern: ${fr.regex}`\n      });\n      rowHasError = true;\n    }\n\n    // Enum check\n    if (fr.enum && !fr.enum.includes(value)) {\n      errors.push({\n        row: i + 1, field, value,\n        rule: 'enum',\n        message: `Field '${field}' must be one of: ${fr.enum.join(', ')}`\n      });\n      rowHasError = true;\n    }\n  }\n\n  if (rowHasError) invalidRows++;\n  else validRows++;\n}\n\nreturn [{\n  json: {\n    valid: errors.length === 0,\n    summary: {\n      totalRows: data.length,\n      validRows,\n      invalidRows,\n      totalErrors: errors.length\n    },\n    errors\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3506f389-3a17-43ad-aa6f-7a2b0774943e",
      "name": "Respond with Report",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1072,
        848
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        },
        "respondWith": "json",
        "responseBody": "={{ $json }}"
      },
      "typeVersion": 1.5
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "312a0c2e-7262-4648-b1c4-f42cba4fd2a1",
  "connections": {
    "Receive Data": {
      "main": [
        [
          {
            "node": "Set Default Rules",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Data": {
      "main": [
        [
          {
            "node": "Respond with Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Default Rules": {
      "main": [
        [
          {
            "node": "Validate Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Send a POST request with a JSON array of records and your validation rules. The workflow checks every field in every row and returns a structured report showing valid/invalid rows with specific error messages.

Source: https://n8n.io/workflows/13999/ — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Data & Sheets

This n8n template helps you manage and validate tokens easily using: n8n as your backend workflow engine Airtable as your lightweight token store Stores user tokens securely in Airtable with expiry or

Airtable, HTTP Request
Data & Sheets

A fully automated workflow that cleans, validates, and restructures your subscriber list using Google Sheets and VerifiEmail. Perfect for marketers, SaaS teams, or anyone maintaining an email database

N8N Nodes Verifiemail, Google Sheets
Data & Sheets

This workflow automatically cleans, validates, and standardizes any CSV file you upload. Perfect for preparing customer lists, sales leads, product catalogs, or any messy datasets before pushing them

Google Drive, Google Sheets
Data & Sheets

This n8n workflow fetches URLs from an RSS feed, checks which URLs have a valid RSS feed and if true, fetches the latest articles from those URLs. It then stores the article details, including the art

RSS Feed Read, Stop And Error, Google Sheets +1
Data & Sheets

This workflow allows you to generate QR codes (Barcodes) in bulk from a Google Sheets file and store the generated QR images automatically in Google Drive. Each QR code contains a unique identifier (i

Google Drive, HTTP Request, Google Sheets