{"record":{"id":"b8737d14c7ccea8a","repo":"getredash/redash","slug":"pagination-fields-should-be-a-list-of-2-field-na","errorCode":null,"errorMessage":"'pagination.fields' should be a list of 2 field names","messagePattern":"'pagination\\.fields' should be a list of 2 field names","errorType":"validation","errorClass":"QueryParseError","httpStatus":null,"severity":"error","filePath":"redash/query_runner/json_ds.py","lineNumber":266,"sourceCode":"    def __init__(self, pagination):\n        self.path = pagination.get(\"path\", \"_links.next.href\")\n        if not isinstance(self.path, str):\n            raise QueryParseError(\"'pagination.path' should be a string\")\n\n    def next(self, url, request_options, response):\n        next_url = _apply_path_search(response, self.path, \"\")\n        if not next_url:\n            return False, None, request_options\n\n        next_url = urljoin(url, next_url)\n        return True, next_url, request_options\n\n\nclass TokenPagination(RequestPagination):\n    def __init__(self, pagination):\n        self.fields = pagination.get(\"fields\", [\"next_page_token\", \"page_token\"])\n        if not isinstance(self.fields, list) or len(self.fields) != 2:\n            raise QueryParseError(\"'pagination.fields' should be a list of 2 field names\")\n\n    def next(self, url, request_options, response):\n        next_token = _apply_path_search(response, self.fields[0], \"\")\n        if not next_token:\n            return False, None, request_options\n\n        params = request_options.get(\"params\", {})\n\n        # prevent infinite loop that can happen if self.fields[1] is wrong\n        if next_token == params.get(self.fields[1]):\n            raise Exception(\"{} did not change; possible misconfiguration\".format(self.fields[0]))\n\n        params[self.fields[1]] = next_token\n        request_options[\"params\"] = params\n        return True, url, request_options\n\n\nregister(JSON)","sourceCodeStart":248,"sourceCodeEnd":284,"githubUrl":"https://github.com/getredash/redash/blob/ca79fe988d81cdac9675b412f3dfcab107bc1fbc/redash/query_runner/json_ds.py#L248-L284","documentation":"TokenPagination requires 'pagination.fields' to be a list of exactly two strings: the field in the response holding the next-page token (default 'next_page_token') and the request parameter to send it in (default 'page_token'). Any other shape is rejected at construction.","triggerScenarios":"Setting pagination.fields to a string, a list of 1 or 3 items, or a dict, e.g. {\"pagination\": {\"type\": \"token\", \"fields\": \"next_cursor\"}}.","commonSituations":"Developer only specifies the response field and forgets the request param name, or copies field names from the API docs as a single string instead of a two-element list.","solutions":["Use a 2-element list: [response_field, request_param], e.g. [\"next_cursor\", \"cursor\"]","Omit fields to use the defaults [\"next_page_token\", \"page_token\"]","Confirm both names against the API's response body and accepted query parameters"],"exampleFix":"// before\n{\"pagination\": {\"type\": \"token\", \"fields\": \"next_cursor\"}}\n// after\n{\"pagination\": {\"type\": \"token\", \"fields\": [\"next_cursor\", \"cursor\"]}}","handlingStrategy":"validation","validationCode":"fields = cfg.get('pagination', {}).get('fields', ['next_page_token', 'page_token'])\nassert isinstance(fields, list) and len(fields) == 2 and all(isinstance(f, str) for f in fields)","typeGuard":"def valid_fields(p: dict) -> bool:\n    f = p.get('fields', ['next_page_token', 'page_token'])\n    return isinstance(f, list) and len(f) == 2 and all(isinstance(x, str) for x in f)","tryCatchPattern":null,"preventionTips":["Document that fields = [response_field, request_param]","Test the query once in the editor before scheduling"],"tags":["redash","json-datasource","pagination","type-validation"],"backgroundTag":"query-config-validation-failed","analyzedSha":"ca79fe988d81cdac9675b412f3dfcab107bc1fbc","analyzedAt":"2026-08-28T18:32:34.637Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}