HumanSignal/label-studio · error · ValidationError

load_tasks: Data root must be list

Error message

load_tasks: Data root must be list

What it means

ValidationError raised by load_tasks_for_async_import in label_studio/data_import/uploader.py. After resolving tasks from uploaded files, URL, or inline project_import.tasks, the data root must be a JSON list. If the parsed payload is a dict or scalar, the async import aborts with 'load_tasks: Data root must be list'.

Source

Thrown at label_studio/data_import/uploader.py:235

        else:
            could_be_tasks_list = False
            (
                data_keys,
                found_formats,
                tasks,
                file_upload_ids,
                could_be_tasks_list,
            ) = tasks_from_url(file_upload_ids, project_import.project, user, url, could_be_tasks_list)
            if could_be_tasks_list:
                project_import.could_be_tasks_list = True
                project_import.save(update_fields=['could_be_tasks_list'])

    elif project_import.tasks:
        tasks = project_import.tasks

    # check is data root is list
    if not isinstance(tasks, list):
        raise ValidationError('load_tasks: Data root must be list')

    # empty tasks error
    if not tasks:
        raise ValidationError('load_tasks: No tasks added')

    check_max_task_number(tasks)
    return tasks, file_upload_ids, found_formats, list(data_keys)


def load_tasks_for_async_import_streaming(project_import, user, batch_size=1000):
    """Load tasks from different types of request.data / request.files saved in project_import model,
    yielding tasks in batches to reduce memory usage"""
    from django.conf import settings

    if not batch_size:
        batch_size = settings.IMPORT_BATCH_SIZE

    all_file_upload_ids = []

View on GitHub (pinned to 0b49e9b539)

Solutions

  1. Reformat the JSON so the top level is an array: [{...}, {...}].
  2. If your data is {"tasks": [...]}, upload only the inner array (save data['tasks'] to a new file).
  3. Wrap a single record in a list before importing.
  4. If a converter produced the wrapper, fix the export settings to emit a bare records array.

Example fix

// before (rejected)
{"tasks": [{"data": {"text": "hi"}}]}

// after (accepted)
[{"data": {"text": "hi"}}]
Defensive patterns

Strategy: type-guard

Validate before calling

import json
payload = json.load(open(path))
if not isinstance(payload, list):
    # unwrap common wrappers
    payload = payload.get('tasks', [payload] if isinstance(payload, dict) else None)
json.dump(payload, open('import.json', 'w'))

Type guard

def is_task_list(payload):
    return isinstance(payload, list) and all(isinstance(t, dict) for t in payload)

Try / catch

try:
    import_tasks(payload)
except ValidationError as e:
    if 'Data root must be list' in str(e):
        payload = [payload] if isinstance(payload, dict) else payload['tasks']
        import_tasks(payload)
    else:
        raise

Prevention

When it happens

Trigger: async_import_background -> load_tasks_for_async_import where the imported JSON file/payload parses to an object (e.g. {"tasks": [...]} or a single dict) instead of a top-level array.

Common situations: Uploading a JSON file shaped as {"data": {...}} or a config-style object; exporting a single task as an object; user pastes JSON with a wrapper key; migrating from another tool that emits object-wrapped arrays.

Related errors


AI-assisted analysis of HumanSignal/label-studio@0b49e9b539 (2026-08-29). Data as JSON: /api/errors/7e894643a0955b3d. Report an issue: GitHub.