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:
- Use the SurveyJS model to detect unknown properties and element types while loading a schema.
- Use the SurveyJS Linter to find static logic defects such as broken references, dead conditions, and cycles.
- Deploy the SurveyJS JSON Schema Validator on your server to validate schemas and user responses through an HTTP API.
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-coreversion 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:
knownVariablesresolves references such as{userRole}and trigger targets.knownFunctionsprevents custom function calls from being reported as unknown.componentslets 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