Documentation

Validating Complex Surveys

Custom grid validation can check relationships across rows. For grid_select_question, grid_multi_select_question, grid_rating_question, grid_numeric_question, this_or_that_question and this_or_that_rating_question, custom_validator receives the complete parsed grid response in both chat and traditional experiences.

In chat, rows are still asked and parsed individually. The custom grid validator runs after the full response is assembled; a cross-row validation error is presented when the final row is submitted. Do not write a validator that expects only the current row's answer.

Return an error-message string to reject the response, or None to accept it. Values depend on the method: single selections are strings (or integers for sorted choices), multi-select rows contain lists, and numeric grids can contain nested dictionaries when columns are configured. Rating validators may receive strings for Don't know rows. Check the relevant signature in the Survey API reference. This complete-grid contract does not change validators on ordinary single questions or MaxDiff tasks.

Customizing the Validator

Some grid methods provide a default straight-line check. Supplying your own validator replaces that check; write the rules appropriate to the question rather than rejecting repeated answers automatically.

For example, this numeric grid checks that the number of people working from home never exceeds the household total:

python
from survey import Survey s = Survey(**globals()) def validate_household(response): if response["Working from home"] > response["Household total"]: return "The number working from home cannot exceed the household total." return None s.grid_numeric_question( "How many adults are in each group?", rows=["Household total", "Working from home"], row_name="Group", min_max=(0, 20), custom_validator=validate_household, ) s.complete()

{"Household total": 2, "Working from home": 3} fails; {"Household total": 3, "Working from home": 2} passes. The validator has both values even when the survey asks one row at a time.

Example Scenario

For a select grid with rows Quality, Price, Service and Delivery, the response is a dictionary such as:

json
{ "Quality": "Poor", "Price": "Poor", "Service": "Good", "Delivery": "Poor", }

If the study requires at least three distinct answers, count distinct values with dict.fromkeys(); the survey sandbox does not provide the set() builtin:

Code
s.grid_select_question( "Please rate the following aspects of the product", rows=["Quality", "Price", "Service", "Delivery"], row_name="Product Aspect", options=["Poor", "Fair", "Good", "Very Good", "Excellent"], custom_validator=lambda response: ( "Please select at least three different options." if len(dict.fromkeys(response.values())) < 3 else None ), )

Restricting the number of people who choose each option

To permit at most one favorite and at most one least favorite across a select grid:

Code
s.grid_select_question( "How much do you like each fruit?", rows=["Bananas", "Oranges", "Apples"], row_name="Fruit", options=["My least favorite", "Don't like", "Like", "My favorite"], custom_validator=lambda response: ( "Choose at most one least favorite and at most one favorite." if list(response.values()).count("My least favorite") > 1 or list(response.values()).count("My favorite") > 1 else None ), )

How It Works

The function counts answers across every row. It rejects duplicate favorite or least-favorite selections. Zero or one of each passes; this rule does not require respondents to choose either extreme.

This-or-that rating defaults and response keys

this_or_that_rating_question uses stable row1, row2, and subsequent keys in the original row_options order. Use these keys for defaults and when reading the returned DictResponse, in both chat and traditional experiences, including when display order is randomized.

Code
preferences = s.this_or_that_rating_question( "Where do your preferences sit?", row_options=[["Apples", "Oranges"], ["Coke", "Pepsi"]], number_of_points=5, default={"row1": 2, "row2": 4}, randomize=True, ) first_pair_rating = preferences["row1"]

If the respondent selects 2 for the first pair and 4 for the second, the returned values are equivalent to:

json
{"row1": 2, "row2": 4}

Endpoint labels such as Apples to Oranges remain reporting topics; they are not keys in this method's default or returned-response dictionary. Duplicate endpoint pairs still have distinct row keys. Ratings use the configured scale (five points starting at 1 by default); use first_point when the scale should start elsewhere. See this_or_that_rating_question for the full signature and Don't know behavior.