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 and response data:

Validate Survey JSON with a Survey Model

The survey model can be used to check both the survey JSON schema and user responses. The Model constructor checks the schema when creating the model, while the model's setData() method checks response data against that schema.

Validate the Survey JSON Schema

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);
}

Schema validation with the model 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.

Validate Response Data

To check response data against a survey JSON schema, pass the data object to the survey model's setData() method. It applies the same survey logic as assigning an object to the data property, then checks the resulting values and returns an array of issues. Direct assignment to data does not perform these checks.

Check a User Response

The following example checks a response for a survey with an email question. The extra legacyId property produces an unknownProperty issue:

import { Model } from "survey-core";

const survey = new Model(surveyJson);
const response = {
  email: "user@example.com",
  legacyId: 42
};

const issues = survey.setData(response);

issues.forEach(({ type, path, value }) => {
  console.log(type, path, value);
});
// unknownProperty legacyId 42

Each issue includes its type, the path to the affected value, and the value itself. The question property identifies the associated question when available. An empty array means that the enabled checks found no issues.

Configure Data Checks

Pass an options object as the second argument to setData() to configure which issues it reports:

Option Default Reports
reportUnknownProperties true Data properties that do not correspond to a question or another recognized survey result field.
reportInvalidValueTypes true Values whose type or structure does not match the question configuration.
reportInvalidChoiceValues true Values that do not match an available choice, matrix column or row, or rating value.
reportExpressionResultMismatches false Values added, changed, or removed by expressions, defaults, triggers, or other logic applied during loading.

For example, disable unknown-property reporting and enable reporting of changes made during loading:

const issues = survey.setData(response, {
  reportUnknownProperties: false,
  reportExpressionResultMismatches: true
});

For an expressionResultMismatch issue, value contains the original value and expressionResult contains the value stored in the survey after loading. These differences can occur even when the supplied data is valid.

The setData() method does not check required answers or run the validation rules defined in the survey JSON schema. To run those checks, call validate() separately. For details, refer to Data 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. A finding can include a fix that you apply to a copy of the JSON.

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 Closest known name, when the defect looks like a typo.
fix A machine-applicable repair, when the defect has exactly one.
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));

Apply a Fix

When an issue has a single unambiguous repair, the finding includes a fix property. Pass this fix and the survey JSON to applyFix, which returns a repaired copy without modifying the original:

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

const result = lintSurvey(surveyJson);
const finding = result.findings.find(f => f.fix);
if (finding) {
  const fixedJson = applyFix(surveyJson, finding.fix);
}

Apply one fix at a time and lint the repaired JSON before applying another because a fix can change array indexes, making paths in earlier findings outdated.

The following loop applies fixes until no fixable findings remain, linting the updated JSON after each fix:

let current = surveyJson;
while (true) {
  const finding = lintSurvey(current).findings.find(f => f.fix);
  if (!finding) break;
  current = applyFix(current, finding.fix);
}

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 schema checks (jsonErrors) Unknown properties and unknown element types encountered while loading the schema.
Response data checks (setData) Unknown data properties, invalid value types, invalid choice values, and optional reports of changes made during loading.
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.