Survey JSON Validation

Survey JSON validation checks whether a form definition is structurally valid and whether its logic can be resolved before you render the form or store it. Early validation helps prevent silently hidden questions, broken navigation, empty choice lists, and conditions that never fire.

SurveyJS provides several complementary ways to validate a survey JSON schema:

Validate Survey JSON with a Survey Model

Pass a survey JSON schema to the Model constructor to create a model. The Form Library checks the schema for unknown properties and element types while loading it. Inspect the jsonErrors array to access these errors:

import { Model } from "survey-core";

const surveyJson = {
  elements: [{
    type: "text",
    name: "email",
    visiblIf: "{consent} = true"
  }]
};

const survey = new Model(surveyJson);

if (Array.isArray(survey.jsonErrors) && survey.jsonErrors.length > 0) {
  console.error(survey.jsonErrors);
}

Model validation does not detect logic defects. In particular, a visibleIf expression that references a missing question can load without a model error. To check the JSON schema for logic errors, use the linter or server validator after model validation.

Use the SurveyJS Linter

Available in survey-core version 3.0.3 and later.

The SurveyJS Linter statically analyzes a survey JSON schema and reports logic defects before the survey reaches runtime.

The linter can find:

  • Misspelled question references
  • Conditions that can never be true
  • Calculated-value and trigger cycles
  • Invalid choice comparisons
  • Other issues that are not reported by model validation

The linter uses pure functions: they analyze the survey JSON without building a survey model, return findings, and do not modify the input.

Import the Linter

Import linter functions from the survey-core/linter entry point:

import { lintSurvey, renderFindings, getRules } from "survey-core/linter";

Run a Basic Check

To lint a survey JSON schema, pass it to the lintSurvey function.

The example below contains a typo in the visibleIf expression. The linter reports reference/unknown and suggests hasInsurance.

import { lintSurvey, renderFindings } from "survey-core/linter";

const surveyJson = {
  pages: [{
    name: "page1",
    elements: [
      {
        type: "radiogroup",
        name: "hasInsurance",
        choices: ["yes", "no"]
      },
      {
        type: "text",
        name: "provider",
        visibleIf: "{hasInsurnce} = 'yes'"
      }
    ]
  }]
};

const result = lintSurvey(surveyJson);

if (result.errorCount > 0) {
  console.log(renderFindings(result));
}

If the survey JSON schema comes as a string, parse it before passing it to lintSurvey:

const surveyJson = JSON.parse(text);
const result = lintSurvey(surveyJson);

Read Lint Results

lintSurvey returns an object with the following properties:

Property Description
findings Findings that were not suppressed, sorted by JSON path and then rule ID.
errorCount Number of findings with error severity.
warningCount Number of findings with warning severity.
infoCount Number of findings with info severity.
suppressedCount Number of findings filtered out by suppressions.
suppressed Suppressed findings, returned only when reportSuppressed is true.

Each finding is an object with the following properties:

Property Description
ruleId ID of the rule that reported the finding.
severity Finding severity: error, warning, or info.
message Human-readable description of the issue.
path Path to the affected value in the survey JSON (for example pages[0].elements[1].visibleIf or triggers[0].setToName).
elementName Name of the affected survey element, if available.
elementType Type of the affected survey element, if available.
suggestion Suggested fix, if available.
related Related elements or paths, if available.
reproduction Reproduction steps or details that demonstrate the finding, if available.

Use renderFindings to produce a human-readable report:

console.log(renderFindings(result));

Linter Rules

All rules are enabled by default.

Rule ID Default severity Detects
choices/dead-source Error choicesFromQuestion or related properties that cannot supply choices.
cycle/calculated-value Error Calculated values that depend on one another in a cycle.
cycle/trigger Warning Triggers that react to values set by other triggers in a loop.
element/unknown-type Info An unregistered question or element type.
expression/syntax Error Expressions that cannot be parsed.
expression/type-mismatch Warning Operators applied to incompatible value shapes.
expression/unknown-choice Warning Conditions compared with a value that is not among a question's choices.
expression/unknown-function Warning Calls to functions that are not registered or declared as known.
name/duplicate Error Duplicate element names or a name shared by an element and a calculated value.
page/empty Warning Pages or panels with nothing that can ever render.
reference/self Error A visibleIf, enableIf, or requiredIf that references its own element.
reference/unknown Error References to missing questions, panels, pages, calculated values, or variables.
trigger/unknown-target Error Triggers that target a missing question, page, or variable.
trigger/unknown-type Warning Unknown or missing trigger types.

Configure Rule Severity

Rules support the following severity values:

  • "error"
  • "warning"
  • "info"
  • "off"

Errors indicate that an expression or reference cannot be evaluated. Warnings often indicate a condition that evaluates but cannot produce the intended result. Info findings usually mean that the linter needs more information about a custom type.

Use getRules() if you need to access the installed rule registry as { id, defaultSeverity } pairs.

To override severity for individual rules or disable a rule, specify the rules config in the options object passed as the second argument to lintSurvey:

const result = lintSurvey(surveyJson, {
  rules: {
    "page/empty": "off",
    "expression/type-mismatch": "error"
  }
});

Configure Suppressed Rules

Suppressed rules are checked but not included in findings by default and do not break the build.

To configure a suppressed rule, specify its ruleId, elementName (case-insensitive), and/or path. All specified fields must match for the suppression to trigger. A path ending in .* matches that path and everything below it.

Suppressed findings are counted in suppressedCount. Add reportSuppressed: true to inspect them in result.suppressed and include them in rendered output when needed:

const result = lintSurvey(surveyJson, {
  suppress: [
    { ruleId: "reference/unknown", elementName: "legacyPatientId" },
    { ruleId: "page/empty", path: "pages[2].*" }
  ],
  reportSuppressed: true
});

console.log(renderFindings(result, { includeSuppressed: true }));

Prefer a narrow suppression with a reason over disabling a rule globally.

Handle Custom Variables, Functions, and Components

The linter reads customizations from the current survey-core process. Register custom functions and components before calling lintSurvey:

import { ComponentCollection, FunctionFactory } from "survey-core";
import { lintSurvey } from "survey-core/linter";

FunctionFactory.Instance.register("scoreSection", (params) => {
  // ...
  return params[0];
});

const result = lintSurvey(surveyJson);

If linting runs in a separate process, declare runtime-provided names through options instead:

  • knownVariables resolves references such as {userRole} and trigger targets.
  • knownFunctions prevents custom function calls from being reported as unknown.
  • components lets the linter resolve paths into specialized and composite questions, such as {fn.firstName}.
const result = lintSurvey(surveyJson, {
  knownVariables: ["userRole", "tenant.plan"],
  knownFunctions: ["scoreSection"],
  components: {
    fullname: {
      elementsJSON: [
        { type: "text", name: "firstName" },
        { type: "text", name: "lastName" }
      ]
    }
  }
});

Add Linting to a Workflow

Lint Survey JSON Files with Node.js

The linter doesn't provide a built-in CLI, but you can create a Node script to run it and output errors and warnings:

// scripts/lint-surveys.mjs
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { lintSurvey, renderFindings } from "survey-core/linter";

const directory = "surveys";
let failed = 0;

for (const entry of await readdir(directory, { recursive: true })) {
  if (!entry.endsWith(".json")) continue;

  const file = join(directory, entry);
  let surveyJson;

  try {
    surveyJson = JSON.parse(await readFile(file, "utf8"));
  } catch (error) {
    console.error(`${file}: invalid JSON`);
    failed++;
    continue;
  }

  let result;
  try {
    result = lintSurvey(surveyJson);
  } catch (error) {
    console.error(`${file}: expected a Survey JSON object`);
    failed++;
    continue;
  }

  if (result.findings.length === 0) continue;
  console.log(`\n${file}`);
  console.log(renderFindings(result));
  if (result.errorCount > 0) failed++;
}

process.exit(failed > 0 ? 1 : 0);

Add this script to the scripts section of your package.json to run the survey JSON linter with an npm command:

{
  "scripts": {
    "lint:surveys": "node scripts/lint-surveys.mjs"
  }
}

Run Linting in CI

Run the same Node script in CI after dependencies are installed:

- run: npm ci
- run: npm run lint:surveys

Since finding order is stable (results are sorted by path and then rule ID), CI output artifacts are easy to compare between runs.

Validate Survey JSON on Your Server

SurveyJS provides the JSON Schema Validator, which is an open-source backend service that you can deploy as part of your own infrastructure. It validates SurveyJS schemas for structural, syntactic, and logical errors and verifies that user responses conform to those schemas, including required questions and data types.

Run the Service

Install dependencies and start the service locally:

npm i
npm run dev

The service is available at http://localhost:3000. You can also deploy it with Docker:

docker build -t surveyjs-json-schema-validator .
docker run -d -p 3000:3000 surveyjs-json-schema-validator

Refer to the repository README for the latest deployment instructions.

Validate a Schema

Send a survey JSON schema in the body of a POST request to the /schema endpoint. The service returns an empty object when the schema is valid. If validation fails, the response contains an errors array that describes the detected issues.

const surveyJson = {
  elements: [{
    type: "text",
    name: "email",
    isRequired: true
  }]
};

fetch("http://localhost:3000/schema", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify(surveyJson)
})
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error("Request failed:", error));

Validate a User Response

Send the schema and the response object in a POST request to the /response endpoint. The service returns validation errors when the response does not satisfy the schema requirements.

const userResponse = {
  email: "user@example.com"
};

fetch("http://localhost:3000/response", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    schema: surveyJson,
    response: userResponse
  })
})
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error("Request failed:", error));

Combine the Validation Checks

All the checks described above serve different purposes:

Check Use it to find
Model validation (jsonErrors) Unknown properties and unknown element types encountered while loading the schema.
Linter (lintSurvey) Broken references, dead conditions, cycles, invalid choice comparisons, and other static logic defects.
SurveyJS JSON Schema Validator Structural, syntactic, and logical schema errors, plus user-response errors when called through its /response endpoint.

Combine these checks when implementing a survey JSON validation workflow. For example, the linter does not detect a misspelled property such as visiblIf, but jsonErrors reports it as an unknown property. On the other hand, the linter checks the schema for logical issues.

See Also

Send feedback to the SurveyJS team

Need help? Visit our support page

Your cookie settings

We use cookies to make your browsing experience more convenient and personal. Some cookies are essential, while others help us analyse traffic. Your personal data and cookies may be used for ad personalization. By clicking “Accept All”, you consent to the use of all cookies as described in our Terms of Use and Privacy Statement. You can manage your preferences in “Cookie settings.”

Your renewal subscription expires soon.

Since the license is perpetual, you will still have permanent access to the product versions released within the first 12 month of the original purchase date.

If you wish to continue receiving technical support from our Help Desk specialists and maintain access to the latest product updates, make sure to renew your subscription by clicking the "Renew" button below.

Your renewal subscription has expired.

Since the license is perpetual, you will still have permanent access to the product versions released within the first 12 month of the original purchase date.

If you wish to continue receiving technical support from our Help Desk specialists and maintain access to the latest product updates, make sure to renew your subscription by clicking the "Renew" button below.