HumanSignal/label-studio · error · ValidationError
not_a_list
not_a_list
Error message
not a list
What it means
Raised by TaskListSerializer.to_internal_value when the submitted payload is not a JSON list/array. The bulk import API only accepts arrays of task objects; any other JSON type (object, string, number) is rejected with the non-field error 'not a list' (code not_a_list).
Source
Thrown at label_studio/tasks/serializers.py:528
return self.context.get('project')
@staticmethod
def format_error(i, detail, item):
if len(detail) == 1:
code = f' {detail[0].code}' if detail[0].code != 'invalid' else ''
return f'Error{code} at item {i}: {detail[0]} :: {item}'
else:
errors = ', '.join(detail)
codes = [d.code for d in detail]
return f'Errors {codes} at item {i}: {errors} :: {item}'
def to_internal_value(self, data):
"""Body of run_validation for all data items"""
if data is None:
raise ValidationError('All tasks are empty (None)')
if not isinstance(data, list):
raise ValidationError({api_settings.NON_FIELD_ERRORS_KEY: 'not a list'}, code='not_a_list')
if not self.allow_empty and len(data) == 0:
if self.parent and self.partial:
raise SkipField()
raise ValidationError({api_settings.NON_FIELD_ERRORS_KEY: 'empty'}, code='empty')
ret, errors = [], []
self.annotation_count, self.prediction_count = 0, 0
for i, item in enumerate(data):
try:
validated = self.child.validate(item)
except ValidationError as exc:
error = self.format_error(i, exc.detail, item)
errors.append(error)
# do not print to user too many errors
if len(errors) >= 100:
errors[99] = '...'
breakView on GitHub (pinned to 0b49e9b539)
Solutions
- Wrap the task(s) in a JSON array: send [{"data": {...}}] instead of {"data": {...}}
- If importing a single task, still submit it as a one-element list
- Use the Python SDK's create_tasks/import_tasks helpers which accept a list and format correctly
- Check that your client isn't sending multipart/form or raw strings where the endpoint expects a JSON array
Example fix
// before
await fetch(`/api/projects/${id}/import`, { method: 'POST', body: JSON.stringify({ data: { text: 'x' } }) });
// after
await fetch(`/api/projects/${id}/import`, { method: 'POST', body: JSON.stringify([{ data: { text: 'x' } }]) }); Defensive patterns
Strategy: type-guard
Validate before calling
if (!Array.isArray(tasks)) throw new Error('import payload must be a JSON array of task objects'); Type guard
function isTaskList(v) {
return Array.isArray(v) && v.every(t => typeof t === 'object' && t !== null && 'data' in t);
} Try / catch
try {
await importTasks(projectId, payload);
} catch (e) {
if (JSON.stringify(e.response?.data || {}).includes('not a list')) {
payload = Array.isArray(payload) ? payload : [payload]; // wrap single object
return importTasks(projectId, payload);
} throw e;
} Prevention
- Always send an array, even for a single task
- Verify Content-Type is application/json and the body parses as an array
- Use typed SDK helpers rather than hand-rolled fetch bodies
When it happens
Trigger: POST to the task import endpoints with a body like {"data": {...}} (a bare task object instead of a list), a plain string, or a number, so isinstance(data, list) fails.
Common situations: Clients posting a single task object instead of wrapping it in an array; sending CSV-as-string content directly as JSON; SDK misuse where the tasks argument is a dict; older integrations written for a different endpoint shape.
Related errors
- All tasks are empty (None)
- empty
- Task item should be dict
- Unsupported or invalid JSON structure
- Filter value must be a list for `is any of` / `is none of`.
AI-assisted analysis of HumanSignal/label-studio@0b49e9b539 (2026-08-29).
Data as JSON: /api/errors/d869a487150b1bc8.
Report an issue: GitHub.