{
  "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
          }
        ]
      ]
    }
  }
}