HumanSignal/label-studio · error · ValidationError

Task root must be dict with "data", "meta", "annotations", "

Error message

Task root must be dict with "data", "meta", "annotations", "predictions" fields

What it means

When validate() receives neither an object with .data nor a populated self.instance, it falls back to treating the argument as a task-root dict. If the argument is not a dict at all (string, list, int, None-with-.data absent, etc.), this ValidationError explains the expected root shape: a dict optionally containing 'data', 'meta', 'annotations', 'predictions'.

Source

Thrown at label_studio/tasks/validation.py:158

        # self.instance is loaded by get_object of view
        if self.instance and hasattr(self.instance, 'data'):
            if isinstance(self.instance.data, dict):
                data = self.instance.data
            elif isinstance(self.instance.data, str):
                try:
                    data = json.loads(self.instance.data)
                except ValueError as e:
                    raise ValidationError("Can't parse task data: " + extract_message(e))
            else:
                raise ValidationError(
                    'Field "data" must be string or dict, but not "' + type(self.instance.data) + '"'
                )
            self.check_data_and_root(self.instance.project, data)
            return task

        # check task is dict
        if not isinstance(task, dict):
            raise ValidationError('Task root must be dict with "data", "meta", "annotations", "predictions" fields')

        # task[data] | task[annotations] | task[predictions] | task[meta]
        if self.check_allowed(task):
            # task[data]
            self.raise_if_wrong_class(task, 'data', (dict, list))
            self.check_data_and_root(self.project, task['data'])

            # task[annotations]: we can't use AnnotationSerializer for validation
            # because it's much different with validation we need here
            self.raise_if_wrong_class(task, 'annotations', list)
            for annotation in task.get('annotations', []):
                if not isinstance(annotation, dict):
                    logger.warning('Annotation must be dict, but "%s" found', str(type(annotation)))
                    continue

                ok = 'result' in annotation
                if not ok:
                    raise ValidationError('Annotation must have "result" fields')

View on GitHub (pinned to 0b49e9b539)

Solutions

  1. Wrap each item as a dict: {'data': {...}} at minimum
  2. If passing raw data (e.g. {'text': 'x'}), that dict takes the root-assumption path; but non-dicts like 'just text' must be {'data': {'text': 'just text'}}
  3. Ensure the batch is a list of dicts, not a list of strings/lists
  4. Coerce CSV rows with dict(zip(headers, row)) before importing

Example fix

// before
import_tasks(['just some text'])
// after
import_tasks([{'data': {'text': 'just some text'}}])
Defensive patterns

Strategy: validation

Validate before calling

def ensure_task_items(items):
    for i, item in enumerate(items):
        if not isinstance(item, dict):
            raise ValueError(f'item {i} must be a dict, got {type(item).__name__}')

Type guard

def is_task_item(x):
    return isinstance(x, dict)

Try / catch

try:
    import_tasks(raw_items)
except ValidationError as e:
    if 'Task root must be dict' in str(e.detail[0]):
        items = [{'data': {'text': it}} if isinstance(it, str) else {'data': {}} for it in raw_items]
        import_tasks(items)

Prevention

When it happens

Trigger: Calling TaskSerializer/TaskValidator.validate with a bare string ('some text'), a list of values, or any non-dict item inside the import batch; to_internal_value iterates data items and each non-dict item reaches this branch unless check_allowed wraps it first (only dicts reach check_allowed's else path).

Common situations: Import payloads where individual items are plain strings/numbers instead of objects; users passing the whole batch to validate instead of one item; CSV importers emitting rows as lists rather than dicts; SDK misuse passing raw text to import_tasks.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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